guoziyun 1 年間 前
コミット
9ffc133580
100 ファイル変更4541 行追加0 行削除
  1. 13 0
      .gitignore
  2. 1 0
      README.md
  3. 24 0
      app.js
  4. 13 0
      config/app/development.js
  5. 27 0
      config/app/index.js
  6. 13 0
      config/app/jccytech.js
  7. 12 0
      config/app/production.js
  8. 20 0
      config/category.js
  9. 9 0
      config/language.js
  10. 184 0
      config/tag.js
  11. 302 0
      config/translate.js
  12. BIN
      dist/assets/bg/5337063.jpg
  13. BIN
      dist/assets/bg/bg_theme5.jpg
  14. BIN
      dist/assets/bg/image.png
  15. BIN
      dist/assets/bg/seamless.png
  16. BIN
      dist/assets/bg/seamless2.png
  17. BIN
      dist/assets/fonts/numbers_roboto_500.png
  18. BIN
      dist/assets/icon/404-opt-1200.webp
  19. BIN
      dist/assets/icon/favicon.ico
  20. BIN
      dist/assets/icon/logo.png
  21. 0 0
      dist/assets/lottie/splash/data.json
  22. BIN
      dist/assets/lottie/splash/images/img_0.png
  23. BIN
      dist/assets/lottie/splash/images/img_1.png
  24. BIN
      dist/assets/lottie/splash/images/img_10.png
  25. BIN
      dist/assets/lottie/splash/images/img_11.png
  26. BIN
      dist/assets/lottie/splash/images/img_12.png
  27. BIN
      dist/assets/lottie/splash/images/img_13.png
  28. BIN
      dist/assets/lottie/splash/images/img_14.png
  29. BIN
      dist/assets/lottie/splash/images/img_15.png
  30. BIN
      dist/assets/lottie/splash/images/img_16.png
  31. BIN
      dist/assets/lottie/splash/images/img_17.png
  32. BIN
      dist/assets/lottie/splash/images/img_18.png
  33. BIN
      dist/assets/lottie/splash/images/img_19.png
  34. BIN
      dist/assets/lottie/splash/images/img_2.png
  35. BIN
      dist/assets/lottie/splash/images/img_20.png
  36. BIN
      dist/assets/lottie/splash/images/img_21.png
  37. BIN
      dist/assets/lottie/splash/images/img_22.png
  38. BIN
      dist/assets/lottie/splash/images/img_23.png
  39. BIN
      dist/assets/lottie/splash/images/img_24.png
  40. BIN
      dist/assets/lottie/splash/images/img_25.png
  41. BIN
      dist/assets/lottie/splash/images/img_26.png
  42. BIN
      dist/assets/lottie/splash/images/img_27.png
  43. BIN
      dist/assets/lottie/splash/images/img_3.png
  44. BIN
      dist/assets/lottie/splash/images/img_4.png
  45. BIN
      dist/assets/lottie/splash/images/img_5.png
  46. BIN
      dist/assets/lottie/splash/images/img_6.png
  47. BIN
      dist/assets/lottie/splash/images/img_7.png
  48. BIN
      dist/assets/lottie/splash/images/img_8.png
  49. BIN
      dist/assets/lottie/splash/images/img_9.png
  50. 0 0
      dist/assets/main-CmYjyMTz.js
  51. 0 0
      dist/assets/main-DGuEntp1.css
  52. 1 0
      dist/assets/modulepreload-polyfill-B5Qt9EMX.js
  53. 2 0
      dist/assets/svg/language-svgrepo-com.svg
  54. 8 0
      dist/assets/svg/list-down-svgrepo-com.svg
  55. 142 0
      dist/assets/svg/logo.svg
  56. 0 0
      dist/assets/webgl-B37p1J_D.js
  57. 43 0
      dist/play.html
  58. 17 0
      dist/stylesheets/album.css
  59. 63 0
      dist/stylesheets/banner.css
  60. 38 0
      dist/stylesheets/category.css
  61. 68 0
      dist/stylesheets/detail.css
  62. 49 0
      dist/stylesheets/footer.css
  63. 162 0
      dist/stylesheets/header.css
  64. 46 0
      dist/stylesheets/pagination.css
  65. 112 0
      dist/stylesheets/styles.css
  66. 24 0
      dist/stylesheets/tag.css
  67. 219 0
      libs/pager.js
  68. 10 0
      libs/redis.js
  69. 26 0
      libs/utils.js
  70. 10 0
      models/index.js
  71. 7 0
      models/mongoose.js
  72. 45 0
      models/rbac/schema-role.js
  73. 14 0
      models/rbac/schema-token.js
  74. 23 0
      models/rbac/schema-user-role.js
  75. 35 0
      models/rbac/schema-user.js
  76. 49 0
      models/rbac/script/role-init.js
  77. 36 0
      models/schema-album.js
  78. 141 0
      models/schema-art.js
  79. 55 0
      models/schema-translate.js
  80. 1257 0
      package-lock.json
  81. 21 0
      package.json
  82. 443 0
      routes/index.js
  83. 56 0
      routes/proxy.js
  84. 14 0
      test/script.js
  85. 71 0
      test/styles.css
  86. 187 0
      test/tags.html
  87. 29 0
      test/test.html
  88. 19 0
      views/album-section.ejs
  89. 32 0
      views/album.ejs
  90. 28 0
      views/albums.ejs
  91. 69 0
      views/banner.ejs
  92. 31 0
      views/category.ejs
  93. 39 0
      views/detail.ejs
  94. 29 0
      views/footer.ejs
  95. 5 0
      views/head.ejs
  96. 56 0
      views/header.ejs
  97. 16 0
      views/hot-section.ejs
  98. 15 0
      views/index.ejs
  99. 16 0
      views/latest-section.ejs
  100. 45 0
      views/pagination.ejs

+ 13 - 0
.gitignore

@@ -0,0 +1,13 @@
+# Add any directories, files, or patterns you don't want to be tracked by version control
+
+node_modules/
+.DS_Store
+*.log
+*.gzip
+*.zip
+._*
+core.*
+report.*.json
+.vscode/
+zorro/.vscode/
+.eslintrc.js

+ 1 - 0
README.md

@@ -0,0 +1 @@
+# Welcome to Art Number Coloring Web Site !

+ 24 - 0
app.js

@@ -0,0 +1,24 @@
+const express = require('express');
+const path = require('path');
+const app = express();
+const { getLocale } = require('./libs/utils');
+
+// 设置视图引擎为EJS
+app.set('view engine', 'ejs');
+
+// 设置视图目录
+app.set('views', path.join(__dirname, 'views'));
+
+
+app.use(express.static(path.join(__dirname, 'dist')));
+
+
+app.use('/', require('./routes/index'));
+
+app.use('/proxy', require('./routes/proxy'));
+
+// 启动服务器,监听3000端口
+const PORT = process.env.PORT || 3000;
+app.listen(PORT, () => {
+  console.log(`Server is running on http://localhost:${PORT}`);
+});

+ 13 - 0
config/app/development.js

@@ -0,0 +1,13 @@
+module.exports = {
+  type: 'development',
+  cookie: {
+    path: '/',
+    httpOnly: true,
+    secure: false,
+    maxAge: 1000 * 3600 * 24 * 30, // 30 days.
+  },
+  sessionName: 'test_sid',
+  mongodbUrl: 'mongodb://coloring:coloring123.@localhost:62701/artsite?authSource=admin',
+  host: 'http://localhost:3000',
+  resHost: 'http://color.jccytech.cn'
+}

+ 27 - 0
config/app/index.js

@@ -0,0 +1,27 @@
+let configs = {
+  type: 'production',
+  cookie: {
+    path: '/',
+    httpOnly: true,
+    domain: '.pcoloring.com',
+    secure: true,
+    maxAge: 1000 * 3600 * 24 * 3, // 30 days.
+  },
+  sessionName: 'sid',
+  mongodbUrl: 'mongodb://coloring:coloring123.@localhost:62701/artsite?authSource=admin',
+  host: 'http://art.pcoloring.com',
+  resHost: 'http://pcoloring.com',
+}
+
+let node_env = require('process').env.NODE_ENV || 'production';
+try {
+  console.log(`Trying to load env spcified configs: ./${node_env}.js`)
+  let envConfigs = require(`./${node_env}`);
+  configs = Object.assign(configs, envConfigs);
+} catch (err) {
+  console.warn(`Load env specific configs failed: ${err}`);
+}
+
+console.log(configs);
+
+module.exports = configs;

+ 13 - 0
config/app/jccytech.js

@@ -0,0 +1,13 @@
+module.exports = {
+  type: 'jccytech',
+  cookie: {
+    path: '/',
+    httpOnly: true,
+    domain: '.jccytech.cn',
+    secure: true, //behind https nginx
+    maxAge: 1000 * 3600 * 24 * 3, // 3 day.
+  },
+  mongodbUrl: 'mongodb://coloring:coloring123.@localhost:62701/artsite?authSource=admin',
+  host: 'http://color.jccytech.cn',
+  resHost: 'http://color.jccytech.cn',
+}

+ 12 - 0
config/app/production.js

@@ -0,0 +1,12 @@
+module.exports = {
+  type: 'production',
+  cookie: {
+    path: '/',
+    httpOnly: true,
+    domain: '.pcoloring.com',
+    secure: true,
+    maxAge: 1000 * 3600 * 24 * 3, //3 days.
+  },
+  mongodbUrl: 'mongodb://coloring:coloring123.@localhost:62701/artsite?authSource=admin',
+  host: 'http://art.pcoloring.com',
+}

+ 20 - 0
config/category.js

@@ -0,0 +1,20 @@
+let categories = [
+  { id: 'latest', en: 'Latest', zh: '最新', es: 'Nuevo', pt: 'Novos', ja: '最新' },
+  { id: 'data_good', en: 'Hot', zh: '最热', es: 'A la moda', pt: 'Top', ja: 'ホット' },
+  { id: 'scenery', en: 'Scenery', zh: '风景', es: 'Paisaje', pt: 'Cenário', ja: '風景' },
+  { id: 'people', en: 'People', zh: '人物', es: 'Gente', pt: 'Pessoas', ja: '人々' },
+  { id: 'mandala', en: 'Mandalas', zh: '曼陀罗', es: 'Mandala', pt: 'Mandala', ja: 'マンダラ' },
+  { id: 'animal', en: 'Animals', zh: '动物', es: 'Animales', pt: 'Animais', ja: '動物' },
+  { id: 'life', en: 'Life', zh: '生活', es: 'Vida', pt: 'Vida', ja: '生活' },
+  { id: 'fantasy', en: 'Fantasy', zh: '幻想', es: 'Fantasía', pt: 'Fantasia', ja: 'ファンタジー' },
+  { id: 'plant', en: 'Plant', zh: '植物', es: 'Plantas', pt: 'Plantas', ja: '植物' },
+  { id: 'place', en: 'Places', zh: '建筑', es: 'Edificios', pt: 'Edifícios', ja: '建物' },
+  { id: 'simple', en: 'Simple', zh: '新手', es: 'Sencillo', pt: 'Simples', ja: 'ベーシック' },
+  { id: 'culture', en: 'Culture', zh: '文化', es: 'Cultura', pt: 'Cultura', ja: '文化' },
+  { id: 'special_date', en: 'Special Date', zh: '节日', es: 'Celebracion', pt: 'Celebração', ja: 'お祝い' },
+  { id: 'food', en: 'Food', zh: '食物', es: 'Comida', pt: 'Comida', ja: '食べ物' },
+  { id: 'famous', en: 'Famous', zh: '名画', es: 'Pintura famosa', pt: 'Pintura famosa', ja: '有名な絵画' },
+  // { id: 'others', en: 'Others', zh: '其他', es: 'Otras', pt: 'Outras', ja: 'その他' },
+]
+
+module.exports = categories;

+ 9 - 0
config/language.js

@@ -0,0 +1,9 @@
+let languages = [
+  { code: 'en', title: 'English' },
+  { code: 'zh', title: '中文' },
+  { code: 'es', title: 'Español' },
+  { code: 'pt', title: 'Português' },
+  { code: 'ja', title: '日本語' },
+]
+
+module.exports = languages;

+ 184 - 0
config/tag.js

@@ -0,0 +1,184 @@
+let tags = [
+  "animal",
+  "scenery",
+  "people",
+  "life",
+  "plant",
+  "girl",
+  "flower",
+  "fantasy",
+  "data_good",
+  "mandala",
+  "food",
+  "special_date",
+  "nature",
+  "cat",
+  "landscape",
+  "dog",
+  "countryside",
+  "forest",
+  "ban",
+  "flowers",
+  "bird",
+  "river",
+  "mountains",
+  "snow",
+  "recommend",
+  "winter",
+  "house",
+  "village",
+  "heart",
+  "Christmas",
+  "garden",
+  "butterfly",
+  "fashion",
+  "summer",
+  "farm",
+  "boy",
+  "place",
+  "sea",
+  "culture",
+  "car",
+  "horse",
+  "lake",
+  "autumn",
+  "tree",
+  "building",
+  "wild",
+  "woman",
+  "room",
+  "park",
+  "ocean",
+  "meadow",
+  "rabbit",
+  "family",
+  "patterns",
+  "home",
+  "mountain",
+  "halloween",
+  "bridge",
+  "friends",
+  "city",
+  "baby",
+  "sunset",
+  "simple",
+  "boat",
+  "window",
+  "plants",
+  "man",
+  "trees",
+  "fruit",
+  "rose",
+  "vacation",
+  "evening",
+  "castle",
+  "snowman",
+  "street",
+  "tiger",
+  "grass",
+  "lady",
+  "child",
+  "vintage",
+  "holiday",
+  "pasture",
+  "deer",
+  "sweet",
+  "night",
+  "beach",
+  "travel",
+  "dress",
+  "fish",
+  "couple",
+  "view",
+  "fairy",
+  "harvest",
+  "yard",
+  "sky",
+  "toys",
+  "wildflowers",
+  "love",
+  "pumpkin",
+  "water",
+  "transportation",
+  "birds",
+  "rocks",
+  "famous",
+  "downtown",
+  "ice",
+  "bear",
+  "spring",
+  "road",
+  "wolf",
+  "unicorn",
+  "lion",
+  "stone",
+  "magic",
+  "cake",
+  "book",
+  "furniture",
+  "children",
+  "fox",
+  "cartoon",
+  "duck",
+  "owl",
+  "squirrel",
+  "sport",
+  "angel",
+  "cub",
+  "decorations",
+  "pond",
+  "sheep",
+  "fence",
+  "interior",
+  "coastline",
+  "beauty",
+  "chicken",
+  "train",
+  "mermaid",
+  "history",
+  "moon",
+  "picnic",
+  "bedroom",
+  "blooming",
+  "sunflower",
+  "mystery",
+  "lovers",
+  "stairs",
+  "walk",
+  "jungle",
+  "livingroom",
+  "thanksgiving",
+  "kingdom",
+  "mother and daughter",
+  "domestic",
+  "mother",
+  "lotus",
+  "dragon",
+  "panda",
+  "pet",
+  "cottage",
+  "tea",
+  "coast",
+]
+
+function getRandomDarkColor() {
+  // 生成 0 到 127 之间的随机整数,因为较深的颜色分量值较低
+  const r = Math.floor(Math.random() * 222);
+  const g = Math.floor(Math.random() * 222);
+  const b = Math.floor(Math.random() * 222);
+
+  // 将 RGB 值转换为十六进制字符串
+  const toHex = (num) => {
+    const hex = num.toString(16);
+    return hex.length === 1 ? `0${hex}` : hex;
+  };
+
+  // 返回 RGB 十六进制颜色字符串
+  return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
+}
+
+
+tags = tags.map(e => { return { tag: e, color: getRandomDarkColor() } });
+
+
+module.exports = tags;

+ 302 - 0
config/translate.js

@@ -0,0 +1,302 @@
+let titleTest = {
+  zh: '快乐的火鸡',
+  en: 'Happy turkey',
+  es: 'Pavo feliz',
+  pt: 'Peru alegre',
+  ja: '幸せな七面鳥',
+}
+
+let descTest = {
+  zh: '在一个充满秋意的森林里,住着一只快乐的火鸡。它叫小火球,因为它的羽毛就像秋天的枫叶一样火红。感恩节快到了,小火球决定给森林里的朋友们送去祝福。它戴上了一顶漂亮的帽子,精心制作了一块写着 “感恩节快乐” 的牌子,然后兴高采烈地出发了。一路上,它遇到了许多南瓜朋友,它们也都穿着橙色的新衣,仿佛在为节日欢呼。小火球带着满满的热情,将感恩节的快乐传递给每一个它遇到的小伙伴,森林里充满了温馨和欢乐的气氛。',
+  en: 'In a forest full of autumn vibes, there lived a cheerful turkey named Little Fireball because its feathers were as red as autumn maple leaves. Thanksgiving was approaching, and Little Fireball decided to send blessings to its friends in the forest. It put on a beautiful hat and carefully made a sign that read "Happy Thanksgiving", then set off cheerfully. Along the way, it met many pumpkin friends, all of them wearing new orange clothes as if cheering for the festival. Little Fireball, with full enthusiasm, spread the joy of Thanksgiving to every little friend it met, and the forest was filled with a warm and joyful atmosphere.',
+  es: 'En un bosque repleto de ambiente de otoño, vivía un pavo feliz. Se llamaba Bolita de Fuego, porque sus plumas eran tan rojas como las hojas de arce de otoño. El Día de Acción de Gracias se acercaba, y Bolita de Fuego decidió enviar felicitaciones a los amigos del bosque. Se puso un bonito sombrero, preparó con esmero una placa que decía "Feliz Día de Acción de Gracias" y partió lleno de alegría. En el camino, encontró a muchos amigos calabazas, que también estaban vestidos de ropa nueva anaranjada, como si estuvieran festejando el festival. Bolita de Fuego, con una gran pasión, transmitía la alegría del Día de Acción de Gracias a cada compañero que encontraba, y el bosque estaba lleno de un ambiente cálido y alegre.',
+  pt: 'Em uma floresta cheia de vibrações de outono, vivia um peru alegre chamado Pequena Bola de Fogo, pois suas penas eram tão vermelhas quanto as folhas de bordo de outono. O Dia de Ação de Graças estava se aproximando, e o Pequeno Bola de Fogo decidiu enviar bênçãos aos seus amigos na floresta. Ele colocou um chapéu bonito e cuidadosamente fez um sinal que dizia "Feliz Dia de Ação de Graças", depois saiu alegremente. Ao longo do caminho, ele encontrou muitos amigos abóbora, todos eles vestindo roupas novas laranjas, como se estivessem aplaudindo para o festival. O Pequeno Bola de Fogo, com muita entusiasmo, espalhou a alegria do Dia de Ação de Graças para cada pequeno amigo que encontrou, e a floresta ficou cheia de um clima caloroso e alegre.',
+  ja: '秋の気配が満ちた森に、楽しい七面鳥が住んでいました。その七面鳥は「小さな火の玉」と呼ばれていました。なぜなら、その羽は秋の紅葉のように真っ赤だったからです。感謝祭が近づいてきたので、小さな火の玉は森の友達に祝福を送ることにしました。きれいな帽子をかぶり、「Happy Thanksgiving」と書いた看板を丁寧に作り、元気よく出発しました。道中、たくさんのカボチャの友達に出会いました。彼らもみんなオレンジ色の新しい服を着て、まるで祝祭を歓迎しているかのようでした。小さな火の玉は満ち満ちた熱意を持って、出会ったすべての友達に感謝祭の喜びを伝えました。森は温かく楽しい雰囲気に包まれました。',
+}
+
+let homePage = {
+  zh: '首页',
+  en: 'Home Page',
+  es: 'Página de inicio',
+  pt: 'Página inicial',
+  ja: 'ホームページ',
+}
+
+let categoryPage = {
+  zh: '分类页',
+  en: 'Category Page',
+  es: 'Página de Categoría',
+  pt: 'Página de Categoria',
+  ja: 'カテゴリページ',
+}
+
+let tagPage = {
+  zh: '标签页',
+  en: 'Tag Page',
+  es: 'Página de Etiqueta',
+  pt: 'Página de Etiqueta',
+  ja: 'タグページ',
+}
+
+let about = {
+  zh: '关于',
+  en: 'About',
+  es: 'Acerca de',
+  pt: 'Sobre',
+  ja: 'について',
+}
+
+let contactUs = {
+  zh: '联系我们',
+  en: 'Contact Us',
+  es: 'Contáctenos',
+  pt: 'Entre em Contato Conosco',
+  ja: 'お問い合わせ',
+}
+
+let feedback = {
+  zh: '意见反馈',
+  en: 'Feedback',
+  es: 'Retroalimentación',
+  pt: 'Feedback',
+  ja: 'ご意見・ご感想',
+}
+
+let designer = {
+  zh: '设计师',
+  en: 'Designer',
+  es: 'Diseñadora',
+  pt: 'Designer',
+  ja: 'デザイナー',
+}
+
+let publishTime = {
+  zh: '发布时间',
+  en: 'Publish Time',
+  es: 'Tiempo de publicación',
+  pt: 'Tempo de publicação',
+  ja: '公開時刻',
+}
+
+let app = {
+  zh: 'App',
+  en: 'App',
+  es: 'App',
+  pt: 'App',
+  ja: 'APP',
+}
+
+let appDownload = {
+  zh: 'App下载',
+  en: 'App Download',
+  es: 'Descarga de la App',
+  pt: 'Download do App',
+  ja: 'アプリのダウンロード',
+}
+
+let my = {
+  zh: '我的',
+  en: 'My',
+  es: 'Mis',
+  pt: 'Minhas',
+  ja: '私の',
+}
+
+let myWorks = {
+  zh: '我的作品',
+  en: 'My Works',
+  es: 'Mis Obras',
+  pt: 'Minhas Obras',
+  ja: '私の作品',
+}
+
+
+let play = {
+  zh: '开始游戏',
+  en: 'Play',
+  es: 'Jugar',
+  pt: 'Jogar',
+  ja: '遊ぶ',
+}
+
+let tag = {
+  zh: '标签',
+  en: 'Tag',
+  es: 'Etiqueta',
+  pt: 'Etiqueta',
+  ja: 'タグ',
+}
+
+let cuteCat = {
+  zh: '可爱的猫咪',
+  en: 'Cute Cat',
+  es: 'Gato lindo',
+  pt: 'Gato fofo',
+  ja: '可愛い猫',
+}
+
+let cuteCatDescription = {
+  zh: 'Art Number Coloring 是涂画家的乐园,拥有海量的免费的艺术线条底图,等着你来给它们涂上颜色, 现在,让我们从这只可爱的小猫咪入手,开启愉快的填色之旅吧。',
+  en: `Art Number Coloring is a paradise for painters. It has a vast number of free artistic line drawings waiting for you to color them. Now, let's start this delightful coloring journey with this cute little cat.`,
+  es: 'Art Number Coloring es un paraíso para los pintores. Tiene una gran cantidad de dibujos de líneas artísticas gratuitos esperando que los colorees. Ahora, empecemos este delicioso viaje de coloreado con este lindo gatito.',
+  pt: 'Art Number Coloring é um paraíso para os pintores. Tem uma enorme quantidade de desenhos de linhas artísticas gratuitos esperando que você os pinte. Agora, vamos começar esta deliciosa jornada de colorir com este gatinho fofo.',
+  ja: 'Art Number Coloring は画家の楽園です。たくさんの無料のアートラインの下地絵があり、あなたが色を塗るのを待っています。今、この可愛いネコちゃんから楽しい塗り絵の旅を始めましょう。',
+}
+
+let daily = {
+  zh: '每日一图',
+  en: 'Daily',
+  es: 'Diario',
+  pt: 'Diário',
+  ja: '毎日一図',
+}
+
+let album = {
+  zh: '专辑',
+  en: 'Album',
+  es: 'álbum',
+  pt: 'álbum',
+  ja: 'アルバム',
+}
+
+let allAlbums = {
+  zh: '所有专辑',
+  en: 'All Albums',
+  es: 'Todos los álbumes',
+  pt: 'Todos os álbuns',
+  ja: 'すべてのアルバム',
+}
+
+let cuteKids = {
+  zh: '天真孩童',
+  en: 'Naive kids',
+  es: 'Niños ingenuos',
+  pt: 'Crianças ingênuas',
+  ja: '天真な子供',
+}
+
+let animalsHappyTime = {
+  zh: '小动物的欢乐时光',
+  en: 'Happy Times of Small Animals',
+  es: 'Tiempos Felices de los Pequeños Animales',
+  pt: 'Tempos Felizes dos Pequenos Animais',
+  ja: '小動物たちの楽しい時間',
+}
+
+let more = {
+  zh: '更多',
+  en: 'More',
+  es: 'Más',
+  pt: 'Mais',
+  ja: 'もっと',
+}
+
+let latest = {
+  zh: '最新上线',
+  en: 'Latest',
+  es: 'Nuevo',
+  pt: 'Novos',
+  ja: '最新リリース',
+}
+
+let hot = {
+  zh: '热门优选',
+  en: 'Popular Selections',
+  es: 'Selecciones Populares',
+  pt: 'Seleções Populares',
+  ja: '人気の選りすぐり',
+}
+
+let special = {
+  zh: '彩绘专区',
+  en: 'Colored Painting',
+  es: 'Pintura Coloreada',
+  pt: 'Pintura colorida',
+  ja: 'カラー絵画',
+}
+
+let selectAlbums = {
+  zh: '精选专辑',
+  en: 'Selected Albums',
+  es: 'Álbumes Seleccionados',
+  pt: 'Álbuns Selecionados',
+  ja: '精選アルバム',
+}
+
+let total = {
+  zh: '共',
+  en: 'total',
+  es: 'total',
+  pt: 'total',
+  ja: '合計',
+}
+
+let item = {
+  zh: '项',
+  en: 'items',
+  es: 'elementos',
+  pt: 'itens',
+  ja: '項',
+}
+
+let page = {
+  zh: '页',
+  en: 'page',
+  es: 'página',
+  pt: 'página',
+  ja: '頁',
+}
+
+let jumpTo = {
+  zh: '跳至',
+  en: 'Jump to',
+  es: 'Saltar a',
+  pt: 'Pular para',
+  ja: 'ジャンプ トゥ',
+}
+
+let wrongPage = {
+  zh: '请输入有效的页码',
+  en: 'Please enter a valid page number',
+  es: 'Por favor, ingrese un número de página válido',
+  pt: 'Por favor, insira um número de página válido',
+  ja: '有効なページ番号を入力してください',
+}
+
+let translate = {
+  homePage,
+  categoryPage,
+  tagPage,
+  about,
+  feedback,
+  contactUs,
+  designer,
+  publishTime,
+  descTest,
+  titleTest,
+  app,
+  appDownload,
+  my,
+  myWorks,
+  play,
+  tag,
+  cuteCat,
+  cuteCatDescription,
+  daily,
+  album,
+  allAlbums,
+  cuteKids,
+  animalsHappyTime,
+  more,
+  latest,
+  hot,
+  special,
+  selectAlbums,
+  total,
+  item,
+  page,
+  jumpTo,
+  wrongPage,
+}
+
+module.exports = translate;

BIN
dist/assets/bg/5337063.jpg


BIN
dist/assets/bg/bg_theme5.jpg


BIN
dist/assets/bg/image.png


BIN
dist/assets/bg/seamless.png


BIN
dist/assets/bg/seamless2.png


BIN
dist/assets/fonts/numbers_roboto_500.png


BIN
dist/assets/icon/404-opt-1200.webp


BIN
dist/assets/icon/favicon.ico


BIN
dist/assets/icon/logo.png


ファイルの差分が大きいため隠しています
+ 0 - 0
dist/assets/lottie/splash/data.json


BIN
dist/assets/lottie/splash/images/img_0.png


BIN
dist/assets/lottie/splash/images/img_1.png


BIN
dist/assets/lottie/splash/images/img_10.png


BIN
dist/assets/lottie/splash/images/img_11.png


BIN
dist/assets/lottie/splash/images/img_12.png


BIN
dist/assets/lottie/splash/images/img_13.png


BIN
dist/assets/lottie/splash/images/img_14.png


BIN
dist/assets/lottie/splash/images/img_15.png


BIN
dist/assets/lottie/splash/images/img_16.png


BIN
dist/assets/lottie/splash/images/img_17.png


BIN
dist/assets/lottie/splash/images/img_18.png


BIN
dist/assets/lottie/splash/images/img_19.png


BIN
dist/assets/lottie/splash/images/img_2.png


BIN
dist/assets/lottie/splash/images/img_20.png


BIN
dist/assets/lottie/splash/images/img_21.png


BIN
dist/assets/lottie/splash/images/img_22.png


BIN
dist/assets/lottie/splash/images/img_23.png


BIN
dist/assets/lottie/splash/images/img_24.png


BIN
dist/assets/lottie/splash/images/img_25.png


BIN
dist/assets/lottie/splash/images/img_26.png


BIN
dist/assets/lottie/splash/images/img_27.png


BIN
dist/assets/lottie/splash/images/img_3.png


BIN
dist/assets/lottie/splash/images/img_4.png


BIN
dist/assets/lottie/splash/images/img_5.png


BIN
dist/assets/lottie/splash/images/img_6.png


BIN
dist/assets/lottie/splash/images/img_7.png


BIN
dist/assets/lottie/splash/images/img_8.png


BIN
dist/assets/lottie/splash/images/img_9.png


ファイルの差分が大きいため隠しています
+ 0 - 0
dist/assets/main-CmYjyMTz.js


ファイルの差分が大きいため隠しています
+ 0 - 0
dist/assets/main-DGuEntp1.css


+ 1 - 0
dist/assets/modulepreload-polyfill-B5Qt9EMX.js

@@ -0,0 +1 @@
+(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))i(e);new MutationObserver(e=>{for(const r of e)if(r.type==="childList")for(const o of r.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&i(o)}).observe(document,{childList:!0,subtree:!0});function s(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerPolicy&&(r.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?r.credentials="include":e.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function i(e){if(e.ep)return;e.ep=!0;const r=s(e);fetch(e.href,r)}})();

+ 2 - 0
dist/assets/svg/language-svgrepo-com.svg

@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg width="800px" height="800px" viewBox="0 0 24 24" role="img" xmlns="http://www.w3.org/2000/svg" aria-labelledby="languageIconTitle" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none" color="#000000"> <title id="languageIconTitle">Language</title> <circle cx="12" cy="12" r="10"/> <path stroke-linecap="round" d="M12,22 C14.6666667,19.5757576 16,16.2424242 16,12 C16,7.75757576 14.6666667,4.42424242 12,2 C9.33333333,4.42424242 8,7.75757576 8,12 C8,16.2424242 9.33333333,19.5757576 12,22 Z"/> <path stroke-linecap="round" d="M2.5 9L21.5 9M2.5 15L21.5 15"/> </svg>

+ 8 - 0
dist/assets/svg/list-down-svgrepo-com.svg

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path opacity="0.5" d="M21 6L3 6" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round"/>
+<path opacity="0.5" d="M21 10L3 10" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round"/>
+<path opacity="0.5" d="M10 14H3" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round"/>
+<path opacity="0.5" d="M10 18H3" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M14 15L17.5 18L21 15" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 142 - 0
dist/assets/svg/logo.svg

@@ -0,0 +1,142 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 26.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="160px"
+	 height="60px" viewBox="0 0 160 60" style="enable-background:new 0 0 160 60;" xml:space="preserve">
+<style type="text/css">
+	.st0{display:none;fill:#5555FF;}
+	.st1{fill:#5555FF;}
+	.st2{fill:#FFFFFF;}
+	.st3{fill:#F70000;}
+	.st4{fill:#FFC921;}
+	.st5{fill:#009245;}
+	.st6{display:none;fill:#FFC921;}
+	.st7{fill:#D81E1E;}
+	.st8{fill:#F94936;}
+	.st9{display:none;fill:#FFFFFF;}
+	.st10{display:none;fill:#F94936;}
+	.st11{fill:#FF4081;}
+</style>
+<g id="图层_1">
+</g>
+<g id="图层_2">
+</g>
+<g id="图层_3">
+</g>
+<g id="图层_4">
+</g>
+<g id="图层_6">
+</g>
+<g id="图层_5">
+</g>
+<g id="图层_7">
+	<g>
+		<g>
+			<path class="st8" d="M22.5,34.9l-2.2-10.5c-0.7-3.4-3.7-5.9-7.2-5.9c-3.5,0-6.5,2.4-7.2,5.9L3.6,34.9c-0.5,2.3,1.3,4.4,3.6,4.4
+				c1.4,0,2.6-0.7,3.2-1.9l1.2-2.3c0.6-1.1,2.2-1.1,2.8,0l1.1,2.2c0.6,1.2,1.9,2,3.3,2h0.1C21.3,39.4,23,37.2,22.5,34.9z M14.5,30.2
+				c-1.1-0.5-1.2-2-2.3-1.6c-0.9,0.3-0.4,1.1-1.2,1.2c-2.1,0.2-3-4-1-6c2.1-2.1,6.4-1.5,7.2,2.7C17.5,28.5,15.9,30.9,14.5,30.2z"/>
+		</g>
+		<g>
+			<path class="st8" d="M25.4,37.8c-0.3,0-0.6-0.2-0.8-0.5c-0.2-0.3-0.4-0.7-0.5-1.3c-0.1-0.5-0.2-1.2-0.2-1.8
+				c0-0.7-0.1-1.4-0.1-2.1c0-0.5,0-1,0-1.5c0-0.5,0.1-1,0.1-1.4c0.1-0.4,0.2-0.8,0.4-1.1c0.2-0.3,0.5-0.4,0.9-0.4
+				c0.3,0,0.5,0.1,0.7,0.3c0.2,0.2,0.4,0.4,0.4,0.8c0.2-0.3,0.5-0.5,0.9-0.6c0.4-0.1,0.7-0.2,1.1-0.2c0.3,0,0.6,0.1,1,0.3
+				c0.3,0.2,0.6,0.5,0.8,0.9c0.2,0.4,0.3,0.8,0.3,1.3c0,0.4-0.1,0.8-0.3,1.2c-0.2,0.4-0.5,0.6-0.9,0.6c-0.2,0-0.3,0-0.4-0.1
+				c-0.1-0.1-0.2-0.1-0.3-0.2c-0.1-0.1-0.2-0.1-0.4-0.1c-0.4,0-0.7,0.2-1,0.6s-0.5,1-0.5,1.7c0,0.4,0,0.8,0,1.2
+				c0,0.4-0.1,0.8-0.1,1.2c-0.1,0.4-0.2,0.7-0.4,1C26,37.7,25.8,37.8,25.4,37.8z"/>
+			<path class="st8" d="M36,37.8c-0.3,0-0.7-0.1-1.1-0.3c-0.4-0.2-0.7-0.5-1-0.9c-0.3-0.4-0.5-0.8-0.6-1.4c-0.1-0.4-0.2-0.8-0.2-1.3
+				c0-0.5-0.1-1.1-0.1-1.7c0-0.6,0-1.2,0-1.7c-0.4,0-0.8-0.1-1.1-0.1c-0.4-0.1-0.6-0.2-0.9-0.5c-0.2-0.2-0.3-0.6-0.3-1
+				c0-0.6,0.1-1,0.3-1.2c0.2-0.3,0.5-0.5,0.8-0.6c0.4-0.1,0.7-0.1,1.2-0.2c0-0.4,0-0.9,0.1-1.3c0-0.5,0.1-0.9,0.2-1.3
+				c0.1-0.4,0.3-0.7,0.5-1c0.2-0.2,0.5-0.4,0.8-0.4c0.4,0,0.7,0.2,0.9,0.6c0.2,0.4,0.3,0.9,0.4,1.5c0,0.6,0.1,1.2,0.1,1.9
+				c0.5,0,1,0,1.4,0.1c0.4,0.1,0.7,0.2,0.9,0.5s0.3,0.6,0.3,1.1c0,0.5-0.1,0.9-0.4,1.2c-0.3,0.3-0.6,0.4-1,0.5s-0.9,0.1-1.3,0.2
+				c0,0.3,0,0.5,0,0.8c0,0.3,0,0.6,0,0.9c0,0.8,0,1.4,0.1,1.7c0.1,0.3,0.1,0.4,0.2,0.5c0.1,0.1,0.2,0.1,0.4,0.1
+				c0.2,0,0.4,0.1,0.6,0.1c0.2,0.1,0.4,0.2,0.5,0.5c0.1,0.3,0.2,0.6,0.2,1.1c0,0.5-0.2,0.9-0.5,1.1C37,37.7,36.5,37.8,36,37.8z"/>
+			<path class="st8" d="M45.8,37.8c-0.3,0-0.6-0.2-0.8-0.5c-0.2-0.4-0.4-0.8-0.6-1.4c-0.2-0.6-0.3-1.2-0.4-1.9
+				c-0.1-0.7-0.2-1.4-0.2-2.1c0-0.7-0.1-1.3-0.1-1.9c0-0.7,0-1.5,0.1-2.3c0.1-0.8,0.2-1.6,0.4-2.3c0.2-0.7,0.5-1.3,0.8-1.8
+				c0.3-0.5,0.8-0.7,1.3-0.7c0.4,0,0.7,0.2,1,0.5c0.3,0.3,0.5,0.8,0.8,1.4c0.2,0.6,0.4,1.3,0.7,2.1c0.2,0.8,0.4,1.6,0.7,2.5
+				c0-1.2,0-2.3,0.1-3.3c0.1-1,0.3-1.8,0.6-2.4c0.3-0.6,0.7-0.9,1.3-0.9c0.5,0,0.8,0.2,1.1,0.7c0.3,0.5,0.5,1,0.6,1.8
+				c0.1,0.7,0.2,1.5,0.3,2.3c0.1,0.8,0.1,1.6,0.1,2.3c0,0.6,0,1.2-0.1,1.9c-0.1,0.7-0.2,1.4-0.3,2.1c-0.1,0.7-0.3,1.3-0.6,1.9
+				c-0.2,0.6-0.5,1-0.9,1.4c-0.3,0.4-0.8,0.5-1.2,0.5c-0.4,0-0.8-0.2-1.1-0.6c-0.3-0.4-0.6-0.9-0.8-1.5c-0.2-0.6-0.4-1.4-0.6-2.1
+				c-0.2-0.8-0.4-1.6-0.6-2.3c0,1,0,1.9,0,2.7s-0.1,1.5-0.2,2.1c-0.1,0.6-0.3,1-0.5,1.3C46.6,37.7,46.2,37.8,45.8,37.8z"/>
+			<path class="st8" d="M57.7,37.8c-0.7,0-1.4-0.3-1.9-0.8c-0.5-0.5-1-1.2-1.3-2.2c-0.3-0.9-0.4-2-0.4-3.2c0-1.2,0.1-2.1,0.4-2.8
+				c0.3-0.7,0.6-1,1.1-1c0.3,0,0.6,0.1,0.8,0.3c0.2,0.2,0.3,0.4,0.4,0.7c0.1,0.3,0.1,0.6,0.1,0.9c0,0.4,0,0.8-0.1,1.2
+				c0,0.4-0.1,0.8-0.1,1.3c0,0.7,0.1,1.2,0.3,1.5c0.2,0.3,0.4,0.5,0.6,0.5c0.2,0,0.5-0.2,0.6-0.5c0.2-0.3,0.3-0.8,0.3-1.5
+				c0-0.5,0-0.9,0-1.3c0-0.4,0-0.8,0-1.3c0-0.5,0.1-0.9,0.3-1.3c0.2-0.4,0.5-0.6,1-0.6c0.5,0,0.9,0.3,1.1,1c0.3,0.7,0.4,1.6,0.4,2.8
+				c0,1.2-0.1,2.3-0.4,3.2c-0.3,0.9-0.7,1.6-1.2,2.2C59.2,37.6,58.5,37.8,57.7,37.8z"/>
+			<path class="st8" d="M67,37.8c-0.5,0-0.8-0.2-1-0.7c-0.2-0.5-0.2-1.1-0.2-2c0-0.7,0-1.3,0-1.8c0-0.5-0.2-0.7-0.4-0.7
+				c-0.2,0-0.4,0.1-0.4,0.3c-0.1,0.2-0.1,0.5-0.1,0.9c0,0.4,0,0.8,0,1.3c0,0.5,0,0.9-0.1,1.3c0,0.4-0.1,0.7-0.3,1
+				c-0.2,0.2-0.4,0.4-0.8,0.4c-0.3,0-0.6-0.2-0.8-0.5c-0.2-0.4-0.4-0.8-0.5-1.4c-0.1-0.6-0.2-1.2-0.2-1.9c0-0.7-0.1-1.3-0.1-2
+				c0-1.2,0.1-2.2,0.2-3c0.1-0.8,0.5-1.3,1-1.5c0.3-0.1,0.6-0.1,0.8,0c0.3,0.1,0.4,0.3,0.5,0.6c0.1-0.1,0.2-0.1,0.4-0.2
+				c0.1-0.1,0.4-0.1,0.7-0.1c0.3,0,0.6,0.1,0.9,0.3c0.3,0.2,0.5,0.5,0.6,0.9c0.2-0.4,0.5-0.7,0.8-0.9c0.3-0.2,0.6-0.3,1.1-0.3
+				c0.6,0,1.2,0.3,1.6,0.8c0.4,0.5,0.8,1.2,1,2.1c0.2,0.9,0.4,1.9,0.4,2.9c0,0.7,0,1.4-0.1,2c-0.1,0.6-0.2,1.1-0.5,1.5
+				c-0.2,0.4-0.5,0.6-0.9,0.6c-0.4,0-0.6-0.1-0.8-0.4c-0.2-0.2-0.3-0.6-0.3-1c0-0.4-0.1-0.9-0.1-1.3c0-0.5,0-0.9,0-1.3
+				c0-0.4-0.1-0.7-0.1-0.9c-0.1-0.2-0.2-0.3-0.4-0.3c-0.2,0-0.3,0.1-0.3,0.3c-0.1,0.2-0.1,0.5-0.1,0.9c0,0.4,0,0.8,0,1.3
+				c0,0.8-0.1,1.5-0.3,2C67.8,37.6,67.5,37.8,67,37.8z"/>
+			<path class="st8" d="M76.5,37.8c-0.7,0-1.3-0.1-1.8-0.4c-0.5-0.3-0.9-0.7-1.2-1.1c-0.3-0.5-0.6-1-0.7-1.5
+				c-0.2-0.6-0.3-1.1-0.4-1.6c-0.1-0.5-0.1-1-0.1-1.5c0-0.4,0-0.8,0-1c0-0.7,0-1.3,0.1-2c0-0.7,0.1-1.4,0.2-2.1s0.3-1.3,0.4-1.8
+				c0.2-0.5,0.4-1,0.7-1.3c0.3-0.3,0.6-0.5,0.9-0.5c0.3,0,0.6,0.1,0.9,0.4c0.3,0.3,0.4,0.8,0.4,1.4c0,0.6-0.1,1.2-0.2,1.8
+				c-0.1,0.6-0.2,1.2-0.3,1.8c0.2-0.2,0.5-0.3,0.7-0.4c0.3-0.1,0.5-0.1,0.8-0.1c0.5,0,1,0.2,1.5,0.5c0.5,0.3,0.9,0.9,1.2,1.6
+				c0.3,0.7,0.5,1.6,0.5,2.7c0,0.9-0.1,1.7-0.4,2.5c-0.3,0.8-0.7,1.4-1.2,1.9C77.9,37.6,77.2,37.8,76.5,37.8z M76.4,34.5
+				c0.3,0,0.5-0.1,0.7-0.4c0.2-0.3,0.3-0.6,0.3-1c0-0.4-0.1-0.7-0.3-1c-0.2-0.3-0.4-0.4-0.7-0.4s-0.5,0.1-0.7,0.4
+				c-0.2,0.3-0.3,0.6-0.3,1c0,0.4,0.1,0.7,0.3,1C75.9,34.3,76.2,34.5,76.4,34.5z"/>
+			<path class="st8" d="M84.3,37.8c-0.7,0-1.4-0.2-2-0.6c-0.6-0.4-1-1-1.3-1.7s-0.5-1.6-0.5-2.5c0-0.9,0.2-1.8,0.5-2.5
+				c0.3-0.8,0.7-1.4,1.3-1.9c0.6-0.5,1.2-0.7,1.9-0.7c0.7,0,1.2,0.2,1.7,0.6c0.5,0.4,0.8,0.9,1.1,1.5c0.3,0.6,0.4,1.2,0.4,1.9
+				c0,0.7-0.1,1.2-0.3,1.6c-0.2,0.4-0.5,0.7-0.8,1c-0.3,0.2-0.7,0.4-1.1,0.4c-0.4,0.1-0.7,0.1-1.1,0c-0.3,0-0.6-0.1-0.9-0.2
+				c0.2,0.4,0.4,0.7,0.7,0.8c0.3,0.1,0.6,0.2,0.9,0.2c0.3,0,0.6-0.1,0.8-0.1c0.3-0.1,0.6-0.1,0.8,0.1s0.3,0.4,0.3,0.7
+				c0,0.3-0.1,0.5-0.2,0.8c-0.1,0.3-0.4,0.5-0.7,0.6C85.4,37.7,84.9,37.8,84.3,37.8z M83.3,31.9c0.1,0.2,0.2,0.4,0.4,0.5
+				c0.2,0.1,0.4,0.1,0.6,0.1c0.2,0,0.4-0.1,0.5-0.2c0.1-0.1,0.2-0.3,0.2-0.5c0-0.2-0.1-0.5-0.2-0.6c-0.1-0.2-0.3-0.3-0.6-0.3
+				c-0.3,0-0.5,0.1-0.6,0.4C83.3,31.4,83.3,31.7,83.3,31.9z"/>
+			<path class="st8" d="M89.6,37.8c-0.3,0-0.6-0.2-0.8-0.5c-0.2-0.3-0.4-0.7-0.5-1.3c-0.1-0.5-0.2-1.2-0.2-1.8
+				c0-0.7-0.1-1.4-0.1-2.1c0-0.5,0-1,0-1.5c0-0.5,0.1-1,0.1-1.4c0.1-0.4,0.2-0.8,0.4-1.1c0.2-0.3,0.5-0.4,0.9-0.4
+				c0.3,0,0.5,0.1,0.7,0.3c0.2,0.2,0.4,0.4,0.4,0.8c0.2-0.3,0.5-0.5,0.9-0.6c0.4-0.1,0.7-0.2,1.1-0.2c0.3,0,0.6,0.1,1,0.3
+				c0.3,0.2,0.6,0.5,0.8,0.9c0.2,0.4,0.3,0.8,0.3,1.3c0,0.4-0.1,0.8-0.3,1.2c-0.2,0.4-0.5,0.6-0.9,0.6c-0.2,0-0.3,0-0.4-0.1
+				c-0.1-0.1-0.2-0.1-0.3-0.2c-0.1-0.1-0.2-0.1-0.4-0.1c-0.4,0-0.7,0.2-1,0.6s-0.5,1-0.5,1.7c0,0.4,0,0.8,0,1.2s-0.1,0.8-0.1,1.2
+				c-0.1,0.4-0.2,0.7-0.4,1C90.2,37.7,89.9,37.8,89.6,37.8z"/>
+			<path class="st8" d="M105,37.8c-0.9,0-1.8-0.3-2.6-0.8c-0.8-0.5-1.4-1.4-1.8-2.5c-0.5-1.1-0.7-2.5-0.7-4.1c0-1,0.1-1.9,0.4-2.8
+				c0.2-0.9,0.6-1.7,1-2.4c0.4-0.7,0.9-1.2,1.5-1.6c0.6-0.4,1.2-0.6,1.9-0.6c0.9,0,1.7,0.2,2.3,0.7c0.7,0.5,1.2,1.1,1.6,1.9
+				c0.4,0.8,0.6,1.7,0.6,2.7c0,0.4-0.1,0.7-0.2,1.1c-0.1,0.3-0.3,0.6-0.5,0.9c-0.2,0.2-0.5,0.3-0.8,0.3c-0.3,0-0.6-0.1-0.8-0.2
+				c-0.2-0.1-0.3-0.3-0.5-0.5c-0.1-0.2-0.3-0.4-0.4-0.6c-0.1-0.2-0.3-0.4-0.5-0.5c-0.2-0.1-0.4-0.2-0.8-0.2c-0.3,0-0.6,0.1-0.8,0.3
+				c-0.3,0.2-0.5,0.5-0.6,0.9s-0.2,0.8-0.2,1.3c0,0.7,0.1,1.4,0.4,1.9c0.3,0.6,0.7,0.8,1.2,0.8c0.4,0,0.7-0.1,0.9-0.3
+				s0.4-0.4,0.6-0.6c0.2-0.2,0.3-0.4,0.5-0.6c0.2-0.2,0.4-0.3,0.8-0.3c0.4,0,0.8,0.2,1,0.6c0.2,0.4,0.3,0.9,0.3,1.5
+				c0,0.6-0.2,1.2-0.6,1.7c-0.3,0.6-0.8,1-1.3,1.3C106.4,37.6,105.8,37.8,105,37.8z"/>
+			<path class="st8" d="M113.1,37.8c-0.7,0-1.4-0.2-1.9-0.6c-0.6-0.4-1-1.1-1.3-1.8c-0.3-0.8-0.5-1.6-0.5-2.6c0-1,0.1-1.8,0.5-2.6
+				c0.3-0.7,0.8-1.3,1.3-1.8s1.2-0.7,1.9-0.7c0.7,0,1.3,0.2,1.9,0.6c0.6,0.4,1,1,1.3,1.8c0.3,0.7,0.5,1.6,0.5,2.6
+				c0,0.9-0.1,1.8-0.5,2.5c-0.3,0.8-0.8,1.4-1.3,1.8C114.5,37.6,113.8,37.8,113.1,37.8z M113.1,34.8c0.3,0,0.5-0.1,0.7-0.4
+				c0.2-0.3,0.3-0.6,0.3-1c0-0.4-0.1-0.7-0.3-1c-0.2-0.3-0.4-0.4-0.7-0.4s-0.5,0.1-0.7,0.4c-0.2,0.3-0.3,0.6-0.3,1
+				c0,0.4,0.1,0.7,0.3,1C112.6,34.6,112.8,34.8,113.1,34.8z"/>
+			<path class="st8" d="M119.3,37.8c-0.5,0-0.9-0.3-1.1-0.8c-0.2-0.5-0.4-1.4-0.5-2.5c-0.1-1.1-0.1-2.5-0.1-4.1c0-1.1,0-2.1,0-3
+				c0-0.9,0.1-1.7,0.2-2.3c0.1-0.7,0.3-1.2,0.5-1.5c0.2-0.4,0.6-0.5,1-0.5s0.8,0.2,1,0.5c0.2,0.4,0.4,0.9,0.5,1.5
+				c0.1,0.7,0.2,1.4,0.2,2.3c0,0.9,0,1.9,0,3c0,2.5-0.1,4.3-0.4,5.6C120.5,37.2,120,37.8,119.3,37.8z"/>
+			<path class="st8" d="M125.2,37.8c-0.7,0-1.4-0.2-1.9-0.6c-0.6-0.4-1-1.1-1.3-1.8c-0.3-0.8-0.5-1.6-0.5-2.6c0-1,0.1-1.8,0.5-2.6
+				s0.8-1.3,1.3-1.8s1.2-0.7,1.9-0.7c0.7,0,1.3,0.2,1.9,0.6c0.6,0.4,1,1,1.3,1.8c0.3,0.7,0.5,1.6,0.5,2.6c0,0.9-0.1,1.8-0.5,2.5
+				c-0.3,0.8-0.8,1.4-1.3,1.8C126.6,37.6,126,37.8,125.2,37.8z M125.3,34.8c0.3,0,0.5-0.1,0.7-0.4c0.2-0.3,0.3-0.6,0.3-1
+				c0-0.4-0.1-0.7-0.3-1c-0.2-0.3-0.4-0.4-0.7-0.4s-0.5,0.1-0.7,0.4c-0.2,0.3-0.3,0.6-0.3,1c0,0.4,0.1,0.7,0.3,1
+				C124.8,34.6,125,34.8,125.3,34.8z"/>
+			<path class="st8" d="M131.2,37.8c-0.3,0-0.6-0.2-0.8-0.5c-0.2-0.3-0.4-0.7-0.5-1.3c-0.1-0.5-0.2-1.2-0.2-1.8
+				c0-0.7-0.1-1.4-0.1-2.1c0-0.5,0-1,0-1.5c0-0.5,0.1-1,0.1-1.4c0.1-0.4,0.2-0.8,0.4-1.1c0.2-0.3,0.5-0.4,0.9-0.4
+				c0.3,0,0.5,0.1,0.7,0.3c0.2,0.2,0.4,0.4,0.4,0.8c0.2-0.3,0.5-0.5,0.9-0.6c0.4-0.1,0.7-0.2,1.1-0.2c0.3,0,0.6,0.1,1,0.3
+				c0.3,0.2,0.6,0.5,0.8,0.9c0.2,0.4,0.3,0.8,0.3,1.3c0,0.4-0.1,0.8-0.3,1.2c-0.2,0.4-0.5,0.6-0.9,0.6c-0.2,0-0.3,0-0.4-0.1
+				c-0.1-0.1-0.2-0.1-0.3-0.2c-0.1-0.1-0.2-0.1-0.4-0.1c-0.4,0-0.7,0.2-1,0.6c-0.3,0.4-0.5,1-0.5,1.7c0,0.4,0,0.8,0,1.2
+				s-0.1,0.8-0.1,1.2c-0.1,0.4-0.2,0.7-0.4,1C131.8,37.7,131.5,37.8,131.2,37.8z"/>
+			<path class="st8" d="M138.4,27.5c-0.5,0-0.9-0.3-1.2-0.8c-0.3-0.5-0.5-1.1-0.5-1.8c0-0.5,0.1-0.9,0.2-1.3
+				c0.2-0.4,0.4-0.7,0.6-0.9c0.3-0.2,0.6-0.4,0.9-0.4c0.3,0,0.6,0.1,0.9,0.4c0.3,0.2,0.5,0.5,0.6,0.9c0.2,0.4,0.2,0.8,0.2,1.3
+				c0,0.5-0.1,0.9-0.2,1.3c-0.2,0.4-0.4,0.7-0.6,0.9C139,27.4,138.7,27.5,138.4,27.5z M138.4,37.8c-0.4,0-0.7-0.2-0.9-0.5
+				c-0.2-0.3-0.3-0.8-0.4-1.5c-0.1-0.7-0.1-1.5-0.1-2.6c0-1.1,0-1.9,0.1-2.6c0.1-0.7,0.2-1.2,0.4-1.5c0.2-0.3,0.5-0.5,0.9-0.5
+				c0.4,0,0.7,0.2,0.9,0.5c0.2,0.3,0.3,0.8,0.4,1.5c0.1,0.7,0.1,1.5,0.1,2.6c0,1.1,0,1.9-0.1,2.6c-0.1,0.7-0.2,1.2-0.4,1.5
+				C139.1,37.7,138.8,37.8,138.4,37.8z"/>
+			<path class="st8" d="M146.6,37.8c-0.4,0-0.7-0.2-0.8-0.5c-0.2-0.3-0.3-0.7-0.3-1.2c0-0.5,0-1,0-1.5c0-0.7-0.1-1.4-0.2-1.8
+				c-0.1-0.5-0.4-0.7-0.8-0.7c-0.3,0-0.6,0.2-0.7,0.7c-0.2,0.5-0.2,1.1-0.2,1.8c0,0.5,0,1,0,1.5c0,0.5-0.1,0.9-0.3,1.2
+				c-0.2,0.3-0.4,0.5-0.9,0.5c-0.3,0-0.6-0.2-0.8-0.5c-0.2-0.3-0.4-0.7-0.5-1.3c-0.1-0.5-0.2-1.1-0.2-1.7c0-0.6-0.1-1.2-0.1-1.8
+				s0-1.1,0.1-1.7c0-0.6,0.1-1.1,0.2-1.7c0.1-0.5,0.3-0.9,0.5-1.2c0.2-0.3,0.5-0.5,0.8-0.5c0.6,0,0.9,0.4,1.1,1.1
+				c0.1-0.2,0.3-0.4,0.6-0.5c0.3-0.2,0.6-0.2,0.9-0.2c0.8,0,1.4,0.3,1.9,0.8c0.5,0.6,0.8,1.3,1.1,2.3s0.3,2.1,0.3,3.4
+				c0,0.7,0,1.2-0.1,1.8c-0.1,0.5-0.2,1-0.4,1.3C147.2,37.7,147,37.8,146.6,37.8z"/>
+			<path class="st8" d="M151.7,41.5c-0.6,0-1-0.2-1.4-0.5c-0.4-0.3-0.6-0.7-0.6-1.2c0-0.5,0.2-1,0.5-1.3c0.3-0.3,0.7-0.4,1.2-0.3
+				c0.5,0.1,1,0.1,1.5-0.1c0.4-0.2,0.7-0.5,0.8-0.9c-0.2,0.1-0.4,0.2-0.7,0.3c-0.3,0.1-0.6,0.1-0.9,0.1c-0.7,0-1.2-0.1-1.7-0.4
+				c-0.5-0.3-0.8-0.7-1.1-1.1c-0.3-0.5-0.5-0.9-0.6-1.5c-0.1-0.5-0.2-1-0.2-1.4c0-1.2,0.2-2.1,0.5-2.9c0.4-0.8,0.8-1.4,1.3-1.8
+				c0.5-0.4,1.1-0.6,1.7-0.6c0.2,0,0.4,0,0.7,0.1c0.3,0,0.5,0.1,0.7,0.2c0.1-0.4,0.2-0.6,0.4-0.9c0.2-0.2,0.5-0.3,0.9-0.3
+				c0.4,0,0.7,0.2,0.9,0.5c0.2,0.3,0.4,0.8,0.6,1.4c0.1,0.6,0.2,1.2,0.3,2c0.1,0.7,0.1,1.5,0.1,2.2c0,0.8,0,1.5-0.1,2.1
+				c-0.1,1.3-0.4,2.5-0.8,3.4c-0.4,1-0.9,1.7-1.6,2.2C153.4,41.2,152.6,41.5,151.7,41.5z M152.3,34.4c0.3,0,0.6-0.2,0.8-0.5
+				c0.2-0.3,0.3-0.7,0.3-1.1c0-0.4-0.1-0.8-0.3-1.1c-0.2-0.3-0.5-0.5-0.8-0.5c-0.3,0-0.6,0.2-0.8,0.5c-0.2,0.3-0.3,0.7-0.3,1.1
+				c0,0.4,0.1,0.8,0.3,1.1C151.7,34.3,152,34.4,152.3,34.4z"/>
+		</g>
+	</g>
+</g>
+</svg>

ファイルの差分が大きいため隠しています
+ 0 - 0
dist/assets/webgl-B37p1J_D.js


ファイルの差分が大きいため隠しています
+ 43 - 0
dist/play.html


+ 17 - 0
dist/stylesheets/album.css

@@ -0,0 +1,17 @@
+
+.album-header {
+  margin-top: 20px;
+  font-family: Arial, sans-serif;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+  .album-header img {
+    width: 90%; /* 手机端宽度占90% */
+  }
+
+}

+ 63 - 0
dist/stylesheets/banner.css

@@ -0,0 +1,63 @@
+.carousel-container {
+  position: relative;
+  width: 60%; /* PC端宽度占60% */
+  margin: auto;
+  overflow: hidden;
+  padding-top: 10px;
+  z-index: 1;
+}
+
+.carousel-images {
+  display: flex;
+  transition: transform 0.5s ease-in-out;
+}
+
+.carousel-images .item {
+  width: 100%;
+  flex-shrink: 0;
+}
+
+.btn-prev, .btn-next {
+  position: absolute;
+  top: 50%;
+  transform: translateY(-50%);
+  background-color: rgba(0,0,0,0.5);
+  color: white;
+  border: none;
+  padding: 10px;
+  cursor: pointer;
+  z-index: 2;
+}
+
+.btn-prev {
+  left: 10px;
+}
+
+.btn-next {
+  right: 10px;
+}
+
+.carousel-play-btn {
+  display: inline-block;
+  padding: 10px;
+  background-color: #ff4081;
+  color: white;
+  text-align: center;
+  text-decoration: none;
+  border-radius: 4px;
+  font-weight: bold;
+  width: 90%;
+}
+
+
+/* 响应式设计, 如果是手机屏幕 */
+@media (max-width: 768px) {
+  .carousel-container {
+      width: 90%; /* 手机端宽度占90% */
+  }
+
+  .cat-description {
+    display: none;
+  }
+
+}

+ 38 - 0
dist/stylesheets/category.css

@@ -0,0 +1,38 @@
+.category {
+  display: flex;
+  height: 50px;
+  justify-content: center;
+  align-items: center;
+  margin-top: 20px;
+  overflow-x: auto; /* 启用水平滚动 */
+  white-space: nowrap; /* 防止内容换行 */
+  -ms-overflow-style: none;  /* IE和Edge浏览器隐藏滚动条 */
+  scrollbar-width: none;  /* Firefox浏览器隐藏滚动条 */
+}
+
+/* 对于Chrome, Safari和Opera浏览器隐藏滚动条 */
+.category::-webkit-scrollbar {
+  display: none;
+}
+
+.category a {
+  margin-right: 10px;
+  text-decoration: none;
+  border: none; 
+  border-radius: 10px;
+  background-color: #f2f2f2; 
+  color: black;
+  padding: 6px 10px 6px 10px; 
+  cursor: pointer;
+  font-size: 14px;
+  font-weight: bold;
+}
+
+.category a.selected {
+  background-color: black; 
+  color: white;
+}
+
+.category a:hover {
+  background-color: #e5e5e5; 
+}

+ 68 - 0
dist/stylesheets/detail.css

@@ -0,0 +1,68 @@
+.details {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: space-between;
+  align-items: flex-start;
+  margin: 20px auto;
+  padding: 20px;
+  max-width: 1200px;
+  border: 1px solid #ccc;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+.poster {
+  flex: 1 1 30%;
+  max-width: 30%;
+  margin-bottom: 20px;
+}
+.poster img {
+  width: 100%;
+  height: auto;
+  border-radius: 4px;
+}
+.tag-button {
+  display: inline-block;
+  text-align: center;
+  text-decoration: none;
+  background-color: #eee;
+  margin: 2px;
+  padding:5px 10px 5px 10px;
+  color: #666;
+}
+
+.tag-button:hover {
+  transform: scale(1.1);
+  color: #ff4081;
+}
+
+.description {
+  flex: 1 1 65%;
+  max-width: 65%;
+}
+.play-button {
+  display: inline-block;
+  margin-top: 20px;
+  padding: 10px 20px;
+  background-color: #ff4081;
+  color: white;
+  text-align: center;
+  text-decoration: none;
+  border-radius: 4px;
+  font-weight: bold;
+}
+
+/* 媒体查询:当屏幕宽度小于768px时(移动端) */
+@media (max-width: 768px) {
+  .poster, .description {
+      flex: 1 1 100%;
+      max-width: 100%;
+      margin-bottom: 20px;
+  }
+  .description {
+      margin-bottom: 0;
+  }
+  .play-button {
+      width: 90%;
+      text-align: center;
+  }
+}

+ 49 - 0
dist/stylesheets/footer.css

@@ -0,0 +1,49 @@
+
+footer {
+  background-color: #333;
+  color: #fff;
+  margin-top: 20px;
+}
+
+.footer-content {
+  display: flex;
+  justify-content: space-around;
+  flex-wrap: wrap;
+  margin: 0 auto;
+  max-width: 1200px;
+}
+
+.footer-section {
+  flex: 1;
+  min-width: 100px;
+  margin: 10px;
+}
+
+.footer-section h3 {
+  margin-bottom: 10px;
+  font-size: 1.2em;
+}
+
+.footer-section p, .footer-section ul {
+  margin: 5px 0;
+}
+
+.footer-section ul {
+  list-style: none;
+  padding: 0;
+}
+
+.footer-section a {
+  color: #fff;
+  text-decoration: none;
+}
+
+.footer-section a:hover {
+  text-decoration: underline;
+}
+
+.footer-bottom {
+  text-align: center;
+  padding: 10px;
+  background-color: #222;
+}

+ 162 - 0
dist/stylesheets/header.css

@@ -0,0 +1,162 @@
+.header {
+  padding: 0px 10px 0px 10px;
+  display: flex;
+  height: 60px;
+  justify-content: space-between;
+  align-items: center;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.header-right {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-around;
+  align-items: center;
+}
+
+.header-right-btn {
+  margin-left: 10px; 
+  text-decoration: none; 
+  color:black;
+  padding: 10px;
+}
+
+.header-right-btn:hover {
+  background-color: #e5e5e5;
+}
+
+.dropdown {
+  position: relative;
+  display: inline-block;
+  z-index: 1000;
+}
+
+.dropdown-content {
+  display: none;
+  position: absolute;
+  background-color: #f9f9f9;
+  min-width: 160px;
+  box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
+  z-index: 1;
+}
+
+.dropdown-content a {
+  color: black;
+  padding: 12px 16px;
+  text-decoration: none;
+  display: block;
+}
+
+.dropdown-content a.selected {
+  background-color: #e5e5e5;
+}
+
+.dropdown-content a:hover {
+  background-color: #f1f1f1;
+}
+
+.dropdown:hover .dropdown-content {
+  display: block;
+}
+
+.dropdown:hover .dropbtn {
+  background-color: #e5e5e5; 
+}
+
+/******************首页的下拉样式****************/
+
+.dropdown-home-content {
+  display: none;
+  position: absolute;
+  padding: 20px 0px 20px 0px;
+  background-color: #2e3030;
+  min-width: 160px;
+  z-index: 1;
+  border: 1px solid #ccc;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.dropdown-home-content a {
+  color: #c5c7c6;
+  padding: 12px 16px;
+  text-decoration: none;
+  display: block;
+}
+
+.dropdown-home-content a.selected {
+  color: #e0ffef;
+  font-weight: bold;
+  font-size: 18px;
+}
+
+.dropdown-home-content a:hover {
+  background-color: #47494a;
+}
+
+.dropdown:hover .dropdown-home-content {
+  display: block;
+}
+
+.divider {
+  height: 1px; /* 设置高度为1像素 */
+  background-color: #47494a; /* 设置背景颜色为黑色 */
+  margin: 5px auto; /* 设置上下外边距 */
+  width: 80%;
+}
+
+.copyright {
+  color: #888888;
+  font-size: 12px;
+  padding-left: 16px;
+}
+
+
+
+/* 默认隐藏搜索框 */
+.search-container {
+  display: none;
+}
+
+
+/* 当屏幕宽度大于768px时显示搜索框 */
+@media (min-width: 768px) {
+  .search-container {
+    display: block;
+    max-width: 600px;
+    min-width: 400px;
+    margin: 0 auto;
+    padding: 16px;
+  }
+
+  .search-box {
+    display: flex;
+    width: 100%;
+    border-radius: 8px;
+    overflow: hidden;
+  }
+
+  .search-box input[type="text"] {
+    flex-grow: 1;
+    padding: 8px;
+    border: none;
+    outline: none;
+    background-color: #f6f6f6;
+  }
+
+  .search-box button {
+    padding: 8px;
+    cursor: pointer;
+    background-color: #f6f6f6;
+    border: none;
+    border-radius: 0 4px 4px 0;
+    transition: background-color 0.3s;
+  }
+
+  .search-box button:hover {
+    background-color: lightblue;
+  }
+}
+
+
+

+ 46 - 0
dist/stylesheets/pagination.css

@@ -0,0 +1,46 @@
+.pagination {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  list-style-type: none;
+  padding: 40px 10px 60px 10px;
+}
+
+.pagination ul {
+  list-style-type: none; /* 移除默认的列表样式 */
+  padding: 0; /* 移除内边距 */
+  margin: 0; /* 移除外边距 */
+  display: flex; /* 使用flexbox布局来实现横向排列 */
+}
+
+.pagination li {
+  margin-right: 10px; /* 每个列表项右侧添加间距 */
+  /* cursor: pointer; */
+}
+
+.pagination li.active {
+  font-weight: bold;
+  color: red; /* 当前页高亮显示 */
+}
+
+.pagination a.active {
+  font-weight: bold;
+  color: #333;
+  text-decoration: none;
+  pointer-events: none;
+}
+
+.pagination input[type="number"] {
+  width: 50px;
+  text-align: center;
+  margin-left: 10px;
+}
+
+.pagination button {
+  margin-left: 10px;
+}
+
+.pagination .ellipsis {
+  margin: 0 10px;
+  color: #888;
+}

+ 112 - 0
dist/stylesheets/styles.css

@@ -0,0 +1,112 @@
+body {
+  background-color: #ffffff;
+  font-family: Arial, sans-serif;
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+
+.content-wrapper {
+  padding: 10px 0px 10px 0px;
+  border: 1px solid #ccc;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.content-title {
+  padding: 0px 20px 0px 20px;
+  display: flex; 
+  flex-direction: row; 
+  justify-content: space-between; 
+  align-items: center;
+}
+
+.content-title a {
+  text-decoration: none;
+  color: green;
+  font-weight: bold;
+}
+
+.content-title a:hover {
+  transform: scale(1.1);
+}
+
+.content {
+  margin-top: 20px;
+  font-family: Arial, sans-serif;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+
+}
+
+.image-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+  gap: 16px;
+  width: 90%;
+}
+
+.image-grid img {
+  width: 100%;
+  height: auto;
+  aspect-ratio: 1;
+  display: block;
+  cursor: pointer;
+  border-radius: 8px; /* 可选,添加圆角效果 */
+  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* 可选,添加阴影效果 */
+}
+
+
+.album-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
+  gap: 16px;
+  width: 90%;
+}
+
+.album-grid-card {
+  width: 100%;
+  height: auto;
+  border: 1px solid #ccc;
+  border-radius: 8px;
+  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+}
+
+.album-grid-card img {
+  width: 100%;
+  height: auto;
+  cursor: pointer;
+  border-radius: 8px;
+}
+
+
+.album-icon-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+  gap: 16px;
+  width: 90%;
+}
+
+/* 响应式设计, 如果是手机屏幕 */
+@media (max-width: 768px) {
+  .image-grid {
+    width: 90%; /* 手机端宽度占90% */
+    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+  }
+  
+
+  .album-grid {
+    width: 90%; /* 手机端宽度占90% */
+    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+  }
+
+
+  .album-icon-grid {
+    width: 90%; /* 手机端宽度占90% */
+    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+  }
+
+
+}

+ 24 - 0
dist/stylesheets/tag.css

@@ -0,0 +1,24 @@
+.tag-cloud {
+  padding: 20px;
+}
+
+.tag-cloud .tag {
+    display: inline-block;
+    text-decoration: none;
+    padding: 5px 10px;
+    margin: 5px;
+    border-radius: 5px;
+    cursor: pointer;
+    background-color: #fefefe;
+    transition: color 0.3s ease;
+}
+
+.tag-cloud a.selected {
+  font-size: 24px;
+  font-weight: bold;
+  background-color: #e0e0e0;
+}
+
+.tag:hover {
+    transform: scale(2.0); /* 悬停时放大 */
+}

+ 219 - 0
libs/pager.js

@@ -0,0 +1,219 @@
+const datefns = require('date-fns');
+const ObjectId = require('mongoose').Types.ObjectId;
+
+/**
+ * Get table header of the model
+ * @param {Model} model 
+ */
+function getHeaders(model, full) {
+  let headers = Object.keys(model.schema.paths).filter((key) => {
+    //TODO 
+    return key.substring(0, 1) != '_';
+  }).map((key) => {
+    let options = model.schema.path(key).options || {};
+    return {
+      data: key,
+      title: options.desc || key,
+      orderable: !(options.orderable === false),
+      searchable: options.searchable || false,
+      listingOrder: options.listingOrder || 1000,
+      listing: options.listing,
+      unit: options.unit,
+      type: getType(options.type),
+    }
+  })
+
+  if (!full) {
+    headers = headers.filter(function (obj) {
+      if (obj.listing === false) {
+        return false;
+      } else return true;
+    });
+  }
+  headers = headers.sort((a, b) => {
+    return a.listingOrder - b.listingOrder;
+  });
+  //console.table(headers);
+  return headers;
+}
+
+function getType(typeFunc) {
+  let type;
+  switch (typeFunc) {
+    case Number:
+      type = "number";
+      break;
+    case String:
+      type = 'string';
+      break;
+    case Date:
+      type = 'date';
+      break;
+    case Boolean:
+      type = 'boolean';
+      break;
+    default:
+      type = "other";
+  }
+  return type;
+}
+
+
+/**
+ * 
+ * @param  {Model} model
+ * @returns 
+ */
+async function getListBuilder(params, model, populates) {
+
+  let { page, length, search, filters } = params;
+
+  //let paths = model.schema.paths;
+  let schema = model.schema;
+
+  let baseQuery = params.base || {};
+  let query = Object.assign({}, baseQuery);
+
+
+  //filters
+  try {
+    // filters = JSON.parse(filters);
+    //console.log('filters: ', filters);
+    Object.keys(filters)
+      .filter(data => schema.paths[data]) //filter not in schema
+      .filter(data => {
+        let value = filters[data];
+        if (value === undefined) return false; //undefined
+        if (Array.isArray(value) && value.length <= 0) return false;
+        return true;
+      }).forEach(data => {
+        //console.log('data', data);
+        let options = schema.path(data).options;
+        //console.log('options', options);
+        let value = filters[data];
+        if (Array.isArray(value) && options.type == Date) {
+          query[data] = {};
+          if (value[0]) {
+            query[data].$gte = datefns.startOfDay(new Date(value[0]));
+          }
+          if (value[1]) {
+            query[data].$lte = datefns.endOfDay(new Date(value[1]));
+          }
+        } else if (Array.isArray(value)) {
+          if (query[data]) {
+            query[data].$in = value;
+          } else {
+            query[data] = { $in: value };
+          }
+
+        } else {
+          query[data] = value;
+        }
+      })
+  } catch (e) {
+    console.warn(e);
+  }
+
+
+  //search
+  if (search) { search = search.trim(); }
+  if (search) {
+    search = escapeRegExp(search);
+    let match = { $regex: new RegExp(search, 'i') };
+    let searchable = getHeaders(model).filter(h => h.searchable);
+    if (searchable.length > 0) {
+      query.$or = searchable.map(header => {
+        let field = {};
+        field[header.data] = match;
+        return field;
+      });
+      if (ObjectId.isValid(search)) {
+        query.$or.push({ _id: search }); // add id search
+      }
+    }
+  }
+
+
+  //console.log('search: ', search);
+  //console.log('query : ', JSON.stringify(query));
+
+  //pagination
+  page = parseInt(page) || 1;
+  if (page <= 0) page = 1;
+  length = parseInt(length) || 30;
+  if (length <= 0) length = 30;
+  else if (length > 500) length = 500;
+
+  let start = (page - 1) * length
+  let recordsTotal = await model.countDocuments(baseQuery);
+  let recordsFiltered = await model.countDocuments(query);
+
+  // console.log('baseQuery', baseQuery);
+  // console.log('query', query);
+
+  /** @type {Query} */
+  let theQuery = model.find(query)
+    .skip(start)
+    .limit(length);
+
+  //order
+  let { orderBy, order } = params;
+  if (orderBy) {
+    if (order != 'asc' && order != 'desc') order = 'desc';
+    let sort = {};
+    sort[orderBy] = order;
+    theQuery.sort(sort)
+  }
+
+  //populate
+  if (populates) {
+    populates.forEach(p => theQuery.populate(p))
+  }
+
+  let data = await theQuery.exec();
+
+  data = data.map(item => item.toObject());
+
+  return { page, length, recordsFiltered, recordsTotal, data, filters, query }
+
+
+}
+
+
+
+function escapeRegExp(str) {
+  var len;
+  var s;
+  var i;
+
+  var RE_CHARS = /[-\/\\^$*+?.()|[\]{}]/g; // eslint-disable-line no-useless-escape
+
+
+  // Check if the string starts with a forward slash...
+  if (str[0] === '/') {
+    // Find the last forward slash...
+    len = str.length;
+    for (i = len - 1; i >= 0; i--) {
+      if (str[i] === '/') {
+        break;
+      }
+    }
+  }
+  // If we searched the string to no avail or if the first letter is not `/`, assume that the string is not of the form `/[...]/[guimy]`:
+  if (i === void 0 || i <= 0) {
+    return str.replace(RE_CHARS, '\\$&');
+  }
+  // We need to de-construct the string...
+  s = str.substring(1, i);
+
+  // Only escape the characters between the `/`:
+  s = s.replace(RE_CHARS, '\\$&');
+
+  // Reassemble:
+  str = str[0] + s + str.substring(i);
+
+  return str;
+}
+
+
+module.exports = { getHeaders, getListBuilder, escapeRegExp }

+ 10 - 0
libs/redis.js

@@ -0,0 +1,10 @@
+const redis = require('redis'),
+  bluebird = require('bluebird');
+
+bluebird.promisifyAll(redis);
+const client = redis.createClient();
+client.on('error', console.error);
+
+
+
+module.exports = client;

+ 26 - 0
libs/utils.js

@@ -0,0 +1,26 @@
+const languages = require('../config/language');
+
+/**
+ * 根据http的Accept-Language返回locale
+ * 有支持中文的则返回zh,其他一律返回en
+ * @param {*} clientLocales 
+ */
+function getLocale(clientLocales) {
+  if (!clientLocales) return 'en'; // 识别不到客户端的locale,则返回默认中文
+  let langs = languages.map(e => e.code);
+  for (let lang of langs) {
+    if (clientLocales[0].includes(lang)) return lang;
+  }
+  return 'en';  // 客户端locale没有中文这一项,那么用英文
+}
+
+
+function ensureLanguage(lang) {
+  let langs = languages.map(e => e.code);
+  if (langs.includes(lang)) return lang;
+  return 'en';
+}
+
+module.exports = {
+  getLocale, ensureLanguage
+}

+ 10 - 0
models/index.js

@@ -0,0 +1,10 @@
+const mongoose = require('./mongoose');
+
+module.exports.User = mongoose.model('User', require('./rbac/schema-user'));
+module.exports.Role = mongoose.model('Role', require('./rbac/schema-role'));
+module.exports.UserRole = mongoose.model('UserRole', require('./rbac/schema-user-role')); //TODO remove
+module.exports.Token = mongoose.model('Token', require('./rbac/schema-token'));
+
+module.exports.Art = mongoose.model('Art', require('./schema-art'));
+module.exports.ArtAlbum = mongoose.model('Album', require('./schema-album'));
+module.exports.Translate = mongoose.model('Translate', require('./schema-translate'));

+ 7 - 0
models/mongoose.js

@@ -0,0 +1,7 @@
+var mongoose = require('mongoose');
+const config = require('../config/app');
+
+mongoose.connect(config.mongodbUrl);
+
+
+module.exports = mongoose;

+ 45 - 0
models/rbac/schema-role.js

@@ -0,0 +1,45 @@
+const Schema = require('mongoose').Schema;
+
+
+let grantSchema = new Schema({
+  resource: { type: String, require: true, desc: '资源' },
+  action: { type: String, enum: ['create', 'read', 'update', 'delete'], require: true, desc: '操作' },
+  possession: { type: String, enum: ['own', 'any'], required: true, desc: '所属' },
+  attributes: { type: String, required: 'true', default: '*', desc: '字段' },
+})
+
+
+let roleSchema = new Schema({
+
+  name: { type: String, required: true, desc: '角色名称', searchable: true, },
+  user: { type: Schema.Types.ObjectId, ref: 'User', desc: '创建者', },
+  permissions: { type: [String], required: true, default: [], desc: '权限', listing: false, },
+  time: { type: Date, default: Date.now, desc: '创建时间', },
+  grants: { type: [grantSchema], default: [], desc: '授权', }
+
+}, {
+  toObject: {
+    transform: roleTransform,
+  },
+  toJSON: {
+    transform: roleTransform,
+  }
+});
+
+
+function roleTransform(doc, ret) {
+  /*
+  if (doc.grants) {
+    doc.grants = doc.grants || [];
+    ret.grants = doc.grants.map(g => auth.base.translate(g));
+    ret.grants = ret.grants.sort((a, b) => {
+      if (a < b) return 1;
+      if (a > b) return -1;
+      return 0;
+    })
+  }
+  */
+}
+
+
+module.exports = roleSchema;

+ 14 - 0
models/rbac/schema-token.js

@@ -0,0 +1,14 @@
+var Schema = require('mongoose').Schema;
+
+
+
+const tokenSchema = new Schema({
+  date: { type: Date, default: Date.now, },
+  email: { type: String, required: true, },
+  expired: { type: Boolean, default: false, },
+  hash: { type: String, required: true, },
+  type: { type: String, required: true, default: 'resetpass', },
+});
+
+
+module.exports = tokenSchema;

+ 23 - 0
models/rbac/schema-user-role.js

@@ -0,0 +1,23 @@
+const Schema = require('mongoose').Schema;
+
+
+let userRoleSchema = new Schema({
+  time: { type: Date, default: Date.now, desc: '授权时间', },
+  authorizer: { type: Schema.Types.ObjectId, ref: 'User', desc: '授权者', },
+  user: { type: Schema.Types.ObjectId, ref: 'User', desc: '用户', required: true, },
+  role: { type: Schema.Types.ObjectId, ref: 'Role', desc: '角色', required: true, },
+}, {
+  toJSON: {}
+});
+
+userRoleSchema.index({
+  user: 1,
+  role: 1,
+  dealer: 1
+}, {
+  unique: true
+});
+
+
+
+module.exports = userRoleSchema

+ 35 - 0
models/rbac/schema-user.js

@@ -0,0 +1,35 @@
+var Schema = require('mongoose').Schema;
+
+const userSchema = new Schema({
+  nickname: { type: String, desc: '昵称', listing: false, },
+  disabled: { type: Boolean, default: false, index: true, desc: '禁用' },
+  name: { type: String, index: true, desc: '姓名', listing: true, searchable: true, },
+  username: { type: String, index: true, unique: true, required: true, desc: '用户名', searchable: true, orderable: true, },
+  password: { type: String, required: true, desc: '密码', listing: false, },
+  phone: { type: String, default: null, index: true, desc: '手机号', searchable: true, orderable: true, },
+  email: { type: String, index: true, desc: 'Email', searchable: true, orderable: true, },
+  createBy: { type: Schema.Types.ObjectId, ref: 'User', index: true, desc: '创建者', },
+
+  roles: { type: [{ type: Schema.Types.ObjectId, ref: 'Role', }], listing: true, desc: '角色', },
+
+  epgs: { type: [String], index: true, trim: true, desc : 'Epg', searchable: true },  // 绑定的epg列表
+
+  administrator: { type: Boolean, default: false, listing: false, desc: '是否管理员(已废弃)', },
+  emailVerified: { type: Boolean, required: true, default: false, desc: 'Email是否验证', listing: false, },
+  dateSignup: { type: Date, required: true, default: Date.now, index: true, desc: '创建时间', },
+  dateLastSignin: { type: Date, default: Date.now, index: true, desc: '最后登录时间', },
+  ipLastSignin: { type: String, default: null, index: true, desc: '最后登录ip', },
+
+
+}, {
+  toJSON: {
+    virtuals: true,
+  },
+  toObject: {
+    virtuals: true,
+  }
+});
+
+module.exports = userSchema;
+
+

+ 49 - 0
models/rbac/script/role-init.js

@@ -0,0 +1,49 @@
+let models = require('../');
+let userModels = require('../../user');
+
+
+
+//create roles
+
+async function getOrCreateSystemAdmin() {
+  let role = await models.Role.findOne({
+    permissions: ['*'],
+  })
+
+  if (role) return role;
+
+  role = models.Role({
+    name: '系统管理员',
+    permissions: ['*'],
+  })
+  role = await role.save();
+  return role;
+}
+
+
+async function authAdmin() {
+  let adminRole = await getOrCreateSystemAdmin();
+  let user = await userModels.User.findOne({
+    username: 'chengen'
+  });
+  let userRole = models.UserRole({
+    user: user,
+    role: adminRole,
+  })
+  return await userRole.save();
+}
+
+
+async function start() {
+  await authAdmin().catch(console.error);
+}
+
+
+
+if (require.main == module) {
+  start()
+    .catch(console.error)
+    .then(() => {
+      require('process').exit()
+    })
+}

+ 36 - 0
models/schema-album.js

@@ -0,0 +1,36 @@
+var Schema = require('mongoose').Schema;
+const config = require('../config/app');
+
+let albumSchema = new Schema({
+  pid: { type: String, require: true, index: true, desc: '产品id' },
+  tag: { type: String, required: true, desc: 'TAG' }, // 专辑唯一标识
+  icon: { type: Schema.Types.ObjectId, ref: 'ArtBin', require: true, desc: 'icon' },
+  cover: { type: Schema.Types.ObjectId, ref: 'ArtBin', require: true, desc: '头图' },
+  enabled: { type: Boolean, default: false, index: true, desc: '启用' },
+  title: { type: Schema.Types.ObjectId, ref: 'Translate', desc: '名称' }, //支持多语言, populate from coloring_ol db
+  slogon: { type: Schema.Types.ObjectId, ref: 'Translate', desc: '口号' }, //支持多语言, populate from coloring_ol db
+  order: { type: Number, default: 1000, index: true, desc: '排序', orderable: true, },
+  contents: { type: [Schema.Types.ObjectId], ref: 'Art', default: [], desc: '内容' }, // 只存内容id,结合popuate
+  timeCreate: { type: Date, index: true, desc: '创建时间', default: Date.now, orderable: true, },
+  timeLastModify: { type: Date, index: true, desc: '最后修改时间', default: Date.now, orderable: true, },
+}, {
+  strict: true,
+  toJSON: {
+    virtuals: true,
+    transform: albumTransform,
+  },
+  toObject: {
+    virtuals: true,
+    transform: albumTransform,
+  },
+
+});
+
+function albumTransform(doc, ret) {
+  ret.icon = `${config.resHost}/res/coloring/album_icon/640/${doc._id}.png`;
+  ret.cover = `${config.resHost}/res/coloring/album_cover/720/${doc._id}.png`;
+  ret.size = doc.contents ? doc.contents.length : 0;
+}
+
+
+module.exports = albumSchema;

+ 141 - 0
models/schema-art.js

@@ -0,0 +1,141 @@
+var Schema = require('mongoose').Schema;
+const config = require('../config/app');
+
+let artSchema = new Schema({
+
+  status: { type: Number, required: true, index: true, orderable: true, default: 1000, desc: '状态' },
+  pageId: { type: Schema.Types.ObjectId, required: true, desc: 'Page Id' }, //old modes.Page._id
+  user: { type: Schema.Types.ObjectId, ref: 'User', required: true, index: true, desc: '作者' },
+
+  org: { type: Schema.Types.ObjectId, ref: 'ArtBin', desc: '线稿原图' },
+  rawOrg: { type: Schema.Types.ObjectId, ref: 'ArtBin', desc: '初始线稿原图' },
+  raw: { type: Schema.Types.ObjectId, ref: 'ArtBin', desc: '矢量线稿', },
+  rawColored: { type: Schema.Types.ObjectId, ref: 'ArtBin', desc: '原始上色效果图' },
+
+  page: { type: Schema.Types.ObjectId, ref: 'ArtBin', required: true, desc: '像素线稿', },
+  pageVersion: { type: Number, default: 1, desc: '底稿版本', },
+  rawMap: { type: Schema.Types.ObjectId, ref: 'ArtBin', desc: '初始Map', },
+
+  colorUser: { type: Schema.Types.ObjectId, ref: 'User', index: true, desc: '填色人', },
+
+  areaCount: { type: Number, required: true, orderable: true, desc: '区块数' },
+  coloredAreaCount: { type: Number, default: 0, desc: '填色区块数' },
+  colorCount: { type: Number, default: 0, desc: '颜色数' },
+  orderedColorCount: { type: Number, default: 0, desc: '已排序颜色数' },
+
+
+  upstream: { type: Boolean, default: false, index: true, desc: '是否推送' },
+  upstreamTime: { type: Date, index: true, desc: '推送时间', orderable: true, },
+  upstreamBy: { type: Schema.Types.ObjectId, ref: 'User', index: true, desc: '推送人' },
+  upstreamFrom: { type: String, index: true, desc: '推送来源' },
+
+  work: { type: Schema.Types.ObjectId, ref: 'ArtBin', desc: '上色效果', },
+  map: { type: Schema.Types.ObjectId, ref: 'ArtBin', required: true, desc: 'Map图', },
+  centers: { type: Schema.Types.ObjectId, ref: 'ArtBin', desc: '填色区域中心点', },
+
+  mapVersion: { type: Number, required: true, default: 1, desc: 'map版本', },
+  workVersion: { type: Number, required: true, default: 1, desc: 'work版本', },
+  centersVersion: { type: Number, required: true, default: 0, desc: '中心点版本', },
+
+
+  hasSpecial: { type: Boolean, desc: '是否有special', default: false, index: true, },
+  special: { type: Schema.Types.ObjectId, ref: 'ArtBin', desc: 'Special图', },
+  specialHalf: { type: Schema.Types.ObjectId, ref: 'ArtBin', desc: 'Special图一半大小', },
+  specialVersion: { type: Number, default: 0, desc: 'Special图版本', },
+  specialOutline: { type: Schema.Types.ObjectId, ref: 'ArtBin', desc: 'Speical切线图', },
+  specialThumb: { type: Schema.Types.ObjectId, ref: 'ArtBin', desc: 'Special上传缩略图' },
+  useSpecialThumb: { type: Number, required: true, default: 0, desc: '当前Special缩略图' },
+
+  mystery: { type: Boolean, default: false, index: true, desc: '神秘图', },
+  ai: { type: Boolean, default: false, index: true, desc: '是否AI参考图', },
+  aiPrompt: { type: String, desc: 'AI关键字' },
+  aiImage: { type: Schema.Types.ObjectId, ref: 'ArtBin', desc: 'AI参考图', },
+
+
+  width: { type: Number, required: true, index: true, desc: '宽' },
+  height: { type: Number, index: true, required: true, desc: '高' },
+  name: { type: String, required: true, desc: '作品名', searchable: true },
+  // 暂时没有用到,预留网站seo
+  desc: { type: String, desc: '作品描述', searchable: true },  // 考虑存json字符串,形如: {zh: '中国', en: 'China'}
+  // 新增字段,与原来的name相区别
+  title: { type: String, desc: '作品标题', searchable: true },
+  use: { type: String, required: true, index: true, default: 'normal', lowercase: true, trim: true, desc: '用途', searchable: true },
+  tags: { type: [String], index: true, lowercase: true, trim: true, desc: '标签', searchable: true },
+
+  epgs: { type: [String], index: true, trim: true, desc: 'EPG', searchable: true },  // 绑定的epg列表, 初始由user表带过来, 后期可再编辑变更
+
+  date: { type: Date, default: Date.now, index: true, desc: '上传时间', orderable: true, },
+  lastMod: { type: Date, default: Date.now, index: true, desc: '修改时间', orderable: true, },
+  timeSubmit: { type: Date, index: true, desc: '提测时间', }, //提测时间
+  timeReady: { type: Date, index: true, desc: '通过时间', }, //通过时间
+
+  refuseReason: { type: String, desc: '拒稿原因' },
+  mark: { type: Schema.Types.ObjectId, ref: 'ArtBin', desc: '标注' },  // 拒稿和打回修改共用
+  markImg: { type: Schema.Types.ObjectId, ref: 'ArtBin', desc: '标注图' }, // 拒稿和打回修改共用
+
+  colorMap: { type: Schema.Types.ObjectId, ref: 'ArtBin', desc: '填色表', },
+  colorSum: { type: Schema.Types.ObjectId, ref: 'ArtBin', desc: '颜色统计', },
+  colorOrder: { type: Schema.Types.ObjectId, ref: 'ArtBin', desc: '填色顺序', },
+  orderAuto: { type: Schema.Types.ObjectId, ref: 'ArtBin', desc: '自动顺序', },
+
+  svgv1: { type: Schema.Types.ObjectId, ref: 'ArtBin', desc: '全矢量化v1', },
+  svgv2: { type: Schema.Types.ObjectId, ref: 'ArtBin', desc: '全矢量化v2', },
+  svgall: { type: Schema.Types.ObjectId, ref: 'ArtBin', desc: '全矢量化all', },
+
+  numberConfig: { type: Schema.Types.ObjectId, ref: 'ArtBin', desc: '填色标签配置', },
+  numberConfigv2: { type: Schema.Types.ObjectId, ref: 'ArtBin', desc: '填色标签配置V2', },
+
+  openglMap: { type: Schema.Types.ObjectId, ref: 'ArtBin', desc: 'opengl专属Map图', },
+  config: { type: Schema.Types.ObjectId, ref: 'ArtBin', desc: 'config.json', }, // opengl配置
+
+  lock: { type: Boolean, default: false, index: true, desc: '是否上锁', },
+  publishVersion: { type: Number, default: 0, index: true, desc: '发布版本' },
+  publishTime: { type: Date, index: true, desc: '发布时间', orderable: true, },
+  publishBy: { type: Schema.Types.ObjectId, ref: 'User', index: true, desc: '发布者' },
+  order: { type: Number, default: 0, index: true, },
+
+
+  drop: { type: Boolean, default: false, index: true, desc: '标记删除' },
+  dropReason: { type: String, desc: '删除原因', },
+  dropBy: { type: Schema.Types.ObjectId, ref: 'User', index: true, desc: '谁标记删除的' },
+
+  publishSchedule: { type: Schema.Types.ObjectId, ref: 'PublishSchedule', index: true, desc: '定时发布' },
+  scheduleToEpg: { type: [String], default: [], desc: '上架计划' },  // 格式: epgId->columnId
+
+  score: { type: Number, orderable: true, desc: '评分' }, // 总体评分0-5
+
+  //数据库内不存储
+  thumb: String,
+  //zip: String,
+
+}, {
+  strict: true,
+  toJSON: {
+    virtuals: true,
+    transform: artTransform,
+  },
+  toObject: {
+    virtuals: true,
+    transform: artTransform,
+  },
+});
+
+
+function artTransform(doc, ret) {
+  //make thumb for art.
+  if (doc.hasSpecial) {
+    doc.thumb = `${config.resHost}/thumbs/v2/special_outline/480/${doc._id}.png`
+  } else {
+    doc.thumb = `${config.resHost}/thumbs/v2/page/480/${doc.pageId}.png`
+  }
+
+}
+
+artSchema.index({
+  order: -1,
+  publishTime: -1
+});
+
+
+
+module.exports = artSchema;

+ 55 - 0
models/schema-translate.js

@@ -0,0 +1,55 @@
+const Schema = require('mongoose').Schema;
+
+module.exports = new Schema({
+  tag: {
+    type: String,
+    required: true,
+    unique: true,
+    desc: 'TAG',
+  },
+
+  id: {   // 这个很特别, 居然是印尼语的简称,不能用做id了,就降格为普通的印尼语,原id换成tag
+    type: String,
+    desc: '印尼语',
+    // required : true, 
+    // unique : true,
+    // desc : 'TAG',
+  },
+
+  valid: {
+    type: Boolean,
+    default: false,
+    desc: '是否可用'
+  },
+
+  en: {
+    type: String,
+    desc: '英文名',
+    // unique: true,
+    required: true,
+  },
+
+  zh: {
+    type: String,
+    desc: '中文名',
+    // unique: true,
+    required: true,
+  },
+
+  timeCreate: {
+    type: Date,
+    desc: '创建时间',
+    default: Date.now,
+    orderable: true,
+  },
+
+  timeLastModify: {
+    type: Date,
+    desc: '最后修改时间',
+    default: Date.now,
+    orderable: true,
+  },
+
+}, {
+  strict: false,
+});

+ 1257 - 0
package-lock.json

@@ -0,0 +1,1257 @@
+{
+  "name": "artsite",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "artsite",
+      "version": "1.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "bluebird": "^3.7.2",
+        "connect-redis": "^3.3.3",
+        "date-fns": "^4.1.0",
+        "ejs": "^3.1.10",
+        "express": "^4.21.2",
+        "mongoose": "^8.9.5",
+        "node-fetch": "^2.7.0"
+      }
+    },
+    "node_modules/@mongodb-js/saslprep": {
+      "version": "1.1.9",
+      "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz",
+      "integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==",
+      "dependencies": {
+        "sparse-bitfield": "^3.0.3"
+      }
+    },
+    "node_modules/@types/webidl-conversions": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
+      "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="
+    },
+    "node_modules/@types/whatwg-url": {
+      "version": "11.0.5",
+      "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz",
+      "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==",
+      "dependencies": {
+        "@types/webidl-conversions": "*"
+      }
+    },
+    "node_modules/accepts": {
+      "version": "1.3.8",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+      "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+      "dependencies": {
+        "mime-types": "~2.1.34",
+        "negotiator": "0.6.3"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/array-flatten": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+      "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
+    },
+    "node_modules/async": {
+      "version": "3.2.6",
+      "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
+      "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+    },
+    "node_modules/bluebird": {
+      "version": "3.7.2",
+      "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
+      "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
+    },
+    "node_modules/body-parser": {
+      "version": "1.20.3",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
+      "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
+      "dependencies": {
+        "bytes": "3.1.2",
+        "content-type": "~1.0.5",
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "destroy": "1.2.0",
+        "http-errors": "2.0.0",
+        "iconv-lite": "0.4.24",
+        "on-finished": "2.4.1",
+        "qs": "6.13.0",
+        "raw-body": "2.5.2",
+        "type-is": "~1.6.18",
+        "unpipe": "1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8",
+        "npm": "1.2.8000 || >= 1.4.16"
+      }
+    },
+    "node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/bson": {
+      "version": "6.10.1",
+      "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.1.tgz",
+      "integrity": "sha512-P92xmHDQjSKPLHqFxefqMxASNq/aWJMEZugpCjf+AF/pgcUpMMQCg7t7+ewko0/u8AapvF3luf/FoehddEK+sA==",
+      "engines": {
+        "node": ">=16.20.1"
+      }
+    },
+    "node_modules/bytes": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+      "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
+      "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/call-bound": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz",
+      "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "get-intrinsic": "^1.2.6"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+    },
+    "node_modules/concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
+    },
+    "node_modules/connect-redis": {
+      "version": "3.4.2",
+      "resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-3.4.2.tgz",
+      "integrity": "sha512-ozA1Z0GDnsCJECfNyNJOqPuW3Fk43fUbKC65Sa/V9hkCBNtXsFU2xtTOVsQGUsflpywuJMgGOV4xrnKzIPFqvA==",
+      "dependencies": {
+        "debug": "^4.1.1",
+        "redis": "^2.8.0"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/connect-redis/node_modules/debug": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+      "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/connect-redis/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+    },
+    "node_modules/content-disposition": {
+      "version": "0.5.4",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+      "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+      "dependencies": {
+        "safe-buffer": "5.2.1"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/content-type": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+      "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie": {
+      "version": "0.7.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+      "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie-signature": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+      "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
+    },
+    "node_modules/date-fns": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
+      "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/kossnocorp"
+      }
+    },
+    "node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/depd": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+      "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/destroy": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+      "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+      "engines": {
+        "node": ">= 0.8",
+        "npm": "1.2.8000 || >= 1.4.16"
+      }
+    },
+    "node_modules/double-ended-queue": {
+      "version": "2.1.0-0",
+      "resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz",
+      "integrity": "sha512-+BNfZ+deCo8hMNpDqDnvT+c0XpJ5cUa6mqYq89bho2Ifze4URTqRkcwR399hWoTrTkbZ/XJYDgP6rc7pRgffEQ=="
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
+    },
+    "node_modules/ejs": {
+      "version": "3.1.10",
+      "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
+      "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
+      "dependencies": {
+        "jake": "^10.8.5"
+      },
+      "bin": {
+        "ejs": "bin/cli.js"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/encodeurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+      "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
+    },
+    "node_modules/etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/express": {
+      "version": "4.21.2",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+      "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
+      "dependencies": {
+        "accepts": "~1.3.8",
+        "array-flatten": "1.1.1",
+        "body-parser": "1.20.3",
+        "content-disposition": "0.5.4",
+        "content-type": "~1.0.4",
+        "cookie": "0.7.1",
+        "cookie-signature": "1.0.6",
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "finalhandler": "1.3.1",
+        "fresh": "0.5.2",
+        "http-errors": "2.0.0",
+        "merge-descriptors": "1.0.3",
+        "methods": "~1.1.2",
+        "on-finished": "2.4.1",
+        "parseurl": "~1.3.3",
+        "path-to-regexp": "0.1.12",
+        "proxy-addr": "~2.0.7",
+        "qs": "6.13.0",
+        "range-parser": "~1.2.1",
+        "safe-buffer": "5.2.1",
+        "send": "0.19.0",
+        "serve-static": "1.16.2",
+        "setprototypeof": "1.2.0",
+        "statuses": "2.0.1",
+        "type-is": "~1.6.18",
+        "utils-merge": "1.0.1",
+        "vary": "~1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.10.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/filelist": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
+      "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
+      "dependencies": {
+        "minimatch": "^5.0.1"
+      }
+    },
+    "node_modules/filelist/node_modules/brace-expansion": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/filelist/node_modules/minimatch": {
+      "version": "5.1.6",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+      "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/finalhandler": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
+      "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
+      "dependencies": {
+        "debug": "2.6.9",
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "on-finished": "2.4.1",
+        "parseurl": "~1.3.3",
+        "statuses": "2.0.1",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/forwarded": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+      "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/fresh": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+      "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.2.7",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz",
+      "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.0.0",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.0",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/http-errors": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+      "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+      "dependencies": {
+        "depd": "2.0.0",
+        "inherits": "2.0.4",
+        "setprototypeof": "1.2.0",
+        "statuses": "2.0.1",
+        "toidentifier": "1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.4.24",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+    },
+    "node_modules/ipaddr.js": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+      "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/jake": {
+      "version": "10.9.2",
+      "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz",
+      "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==",
+      "dependencies": {
+        "async": "^3.2.3",
+        "chalk": "^4.0.2",
+        "filelist": "^1.0.4",
+        "minimatch": "^3.1.2"
+      },
+      "bin": {
+        "jake": "bin/cli.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/kareem": {
+      "version": "2.6.3",
+      "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz",
+      "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==",
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/media-typer": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+      "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/memory-pager": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
+      "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="
+    },
+    "node_modules/merge-descriptors": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+      "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/methods": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+      "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+      "bin": {
+        "mime": "cli.js"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/mongodb": {
+      "version": "6.12.0",
+      "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.12.0.tgz",
+      "integrity": "sha512-RM7AHlvYfS7jv7+BXund/kR64DryVI+cHbVAy9P61fnb1RcWZqOW1/Wj2YhqMCx+MuYhqTRGv7AwHBzmsCKBfA==",
+      "dependencies": {
+        "@mongodb-js/saslprep": "^1.1.9",
+        "bson": "^6.10.1",
+        "mongodb-connection-string-url": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=16.20.1"
+      },
+      "peerDependencies": {
+        "@aws-sdk/credential-providers": "^3.188.0",
+        "@mongodb-js/zstd": "^1.1.0 || ^2.0.0",
+        "gcp-metadata": "^5.2.0",
+        "kerberos": "^2.0.1",
+        "mongodb-client-encryption": ">=6.0.0 <7",
+        "snappy": "^7.2.2",
+        "socks": "^2.7.1"
+      },
+      "peerDependenciesMeta": {
+        "@aws-sdk/credential-providers": {
+          "optional": true
+        },
+        "@mongodb-js/zstd": {
+          "optional": true
+        },
+        "gcp-metadata": {
+          "optional": true
+        },
+        "kerberos": {
+          "optional": true
+        },
+        "mongodb-client-encryption": {
+          "optional": true
+        },
+        "snappy": {
+          "optional": true
+        },
+        "socks": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/mongodb-connection-string-url": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz",
+      "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==",
+      "dependencies": {
+        "@types/whatwg-url": "^11.0.2",
+        "whatwg-url": "^14.1.0 || ^13.0.0"
+      }
+    },
+    "node_modules/mongoose": {
+      "version": "8.9.5",
+      "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.9.5.tgz",
+      "integrity": "sha512-SPhOrgBm0nKV3b+IIHGqpUTOmgVL5Z3OO9AwkFEmvOZznXTvplbomstCnPOGAyungtRXE5pJTgKpKcZTdjeESg==",
+      "dependencies": {
+        "bson": "^6.10.1",
+        "kareem": "2.6.3",
+        "mongodb": "~6.12.0",
+        "mpath": "0.9.0",
+        "mquery": "5.0.0",
+        "ms": "2.1.3",
+        "sift": "17.1.3"
+      },
+      "engines": {
+        "node": ">=16.20.1"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/mongoose"
+      }
+    },
+    "node_modules/mongoose/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+    },
+    "node_modules/mpath": {
+      "version": "0.9.0",
+      "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
+      "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
+      "engines": {
+        "node": ">=4.0.0"
+      }
+    },
+    "node_modules/mquery": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz",
+      "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==",
+      "dependencies": {
+        "debug": "4.x"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/mquery/node_modules/debug": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+      "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/mquery/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+    },
+    "node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+    },
+    "node_modules/negotiator": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+      "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/node-fetch": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+      "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+      "dependencies": {
+        "whatwg-url": "^5.0.0"
+      },
+      "engines": {
+        "node": "4.x || >=6.0.0"
+      },
+      "peerDependencies": {
+        "encoding": "^0.1.0"
+      },
+      "peerDependenciesMeta": {
+        "encoding": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/node-fetch/node_modules/tr46": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
+    },
+    "node_modules/node-fetch/node_modules/webidl-conversions": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
+    },
+    "node_modules/node-fetch/node_modules/whatwg-url": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+      "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+      "dependencies": {
+        "tr46": "~0.0.3",
+        "webidl-conversions": "^3.0.0"
+      }
+    },
+    "node_modules/object-inspect": {
+      "version": "1.13.3",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
+      "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/on-finished": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+      "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+      "dependencies": {
+        "ee-first": "1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/parseurl": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/path-to-regexp": {
+      "version": "0.1.12",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+      "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
+    },
+    "node_modules/proxy-addr": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+      "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+      "dependencies": {
+        "forwarded": "0.2.0",
+        "ipaddr.js": "1.9.1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/punycode": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+      "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/qs": {
+      "version": "6.13.0",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
+      "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
+      "dependencies": {
+        "side-channel": "^1.0.6"
+      },
+      "engines": {
+        "node": ">=0.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/range-parser": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+      "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/raw-body": {
+      "version": "2.5.2",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
+      "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
+      "dependencies": {
+        "bytes": "3.1.2",
+        "http-errors": "2.0.0",
+        "iconv-lite": "0.4.24",
+        "unpipe": "1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/redis": {
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz",
+      "integrity": "sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==",
+      "dependencies": {
+        "double-ended-queue": "^2.1.0-0",
+        "redis-commands": "^1.2.0",
+        "redis-parser": "^2.6.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/redis-commands": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz",
+      "integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ=="
+    },
+    "node_modules/redis-parser": {
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz",
+      "integrity": "sha512-9Hdw19gwXFBJdN8ENUoNVJFRyMDFrE/ZBClPicKYDPwNPJ4ST1TedAHYNSiGKElwh2vrmRGMoJYbVdJd+WQXIw==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ]
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+    },
+    "node_modules/send": {
+      "version": "0.19.0",
+      "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
+      "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
+      "dependencies": {
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "destroy": "1.2.0",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "fresh": "0.5.2",
+        "http-errors": "2.0.0",
+        "mime": "1.6.0",
+        "ms": "2.1.3",
+        "on-finished": "2.4.1",
+        "range-parser": "~1.2.1",
+        "statuses": "2.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/send/node_modules/encodeurl": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+      "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/send/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+    },
+    "node_modules/serve-static": {
+      "version": "1.16.2",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
+      "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
+      "dependencies": {
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "parseurl": "~1.3.3",
+        "send": "0.19.0"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/setprototypeof": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
+    },
+    "node_modules/side-channel": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+      "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3",
+        "side-channel-list": "^1.0.0",
+        "side-channel-map": "^1.0.1",
+        "side-channel-weakmap": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-list": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+      "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-map": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+      "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-weakmap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+      "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3",
+        "side-channel-map": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/sift": {
+      "version": "17.1.3",
+      "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz",
+      "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ=="
+    },
+    "node_modules/sparse-bitfield": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
+      "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
+      "dependencies": {
+        "memory-pager": "^1.0.2"
+      }
+    },
+    "node_modules/statuses": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+      "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/toidentifier": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+      "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+      "engines": {
+        "node": ">=0.6"
+      }
+    },
+    "node_modules/tr46": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz",
+      "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==",
+      "dependencies": {
+        "punycode": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/type-is": {
+      "version": "1.6.18",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+      "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+      "dependencies": {
+        "media-typer": "0.3.0",
+        "mime-types": "~2.1.24"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/utils-merge": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+      "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+      "engines": {
+        "node": ">= 0.4.0"
+      }
+    },
+    "node_modules/vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/webidl-conversions": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+      "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/whatwg-url": {
+      "version": "14.1.0",
+      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.0.tgz",
+      "integrity": "sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==",
+      "dependencies": {
+        "tr46": "^5.0.0",
+        "webidl-conversions": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    }
+  }
+}

+ 21 - 0
package.json

@@ -0,0 +1,21 @@
+{
+  "name": "artsite",
+  "version": "1.0.0",
+  "main": "index.js",
+  "scripts": {
+    "start": "nodemon ./bin/artsite"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "description": "",
+  "dependencies": {
+    "bluebird": "^3.7.2",
+    "connect-redis": "^3.3.3",
+    "date-fns": "^4.1.0",
+    "ejs": "^3.1.10",
+    "express": "^4.21.2",
+    "mongoose": "^8.9.5",
+    "node-fetch": "^2.7.0"
+  }
+}

+ 443 - 0
routes/index.js

@@ -0,0 +1,443 @@
+var express = require('express');
+var router = express.Router();
+const models = require('../models');
+const config = require('../config/app');
+const redis = require('../libs/redis');
+const categories = require('../config/category');
+const tags = require('../config/tag');
+const languages = require('../config/language');
+const translate = require('../config/translate');
+const { getLocale, ensureLanguage } = require('../libs/utils');
+const { format } = require('date-fns');
+const { getListBuilder } = require('../libs/pager');
+
+const CACHE_PREFIX = "art_v1";
+const CACHE_EXPIRES = 60; // 60s刷新一次
+
+
+router.get('/', (req, res, next) => {
+  let locale = getLocale(req.acceptsLanguages());
+  let lang = ensureLanguage(locale);
+  return res.redirect(`/${lang}`);
+});
+
+
+router.get('/:lang/', async (req, res, next) => {
+  let lang = ensureLanguage(req.params.lang);
+
+  let baseSort = { publishTime: 'desc' };
+
+  // 最新上线
+  let latest = await models.Art
+    .find({ status: 9000 })
+    .select('width height date publishTime tags lastMod mystery hasSpecial useSpecialThumb publishVersion lock pageId')
+    .sort(baseSort)
+    .limit(12)
+    .lean()
+    .exec();
+  organizeData(latest);
+
+  // 热门精选
+  let recommend = await models.Art
+    .find({ tags: 'data_good', status: 9000 })
+    .select('width height date publishTime tags lastMod mystery hasSpecial useSpecialThumb publishVersion lock pageId')
+    .sort(baseSort)
+    .limit(12)
+    .lean()
+    .exec();
+  organizeData(recommend);
+
+  // special 专区
+  let special = await models.Art
+    .find({ hasSpecial: true, status: 9000 })
+    .select('width height date publishTime tags lastMod mystery hasSpecial useSpecialThumb publishVersion lock pageId')
+    .sort(baseSort)
+    .limit(12)
+    .lean()
+    .exec();
+  organizeData(special);
+
+  // 专辑
+  let albums = await models.ArtAlbum
+    .find({ pid: 'art', enabled: true })
+    .sort({ order: 'asc' })
+    .populate('title')
+    .populate('slogon')
+    .select('tag title slogon contents')
+    .limit(6)
+    .lean()
+    .exec();
+
+
+  for (let doc of albums) {
+    doc.icon = `${config.resHost}/res/coloring/album_icon/320/${doc._id}.jpeg`;
+    doc.cover = `${config.resHost}/res/coloring/album_cover/640/${doc._id}.jpeg`;
+    doc.title = doc.title ? doc.title[lang] : '';
+    doc.slogon = doc.slogon ? doc.slogon[lang] : '';
+    doc.size = doc.contents.length;
+  }
+
+
+  let data = {
+    title: 'Art Number Coloring',
+    latest,
+    recommend,
+    special,
+    albums,
+    translate,
+    categories,
+    languages,
+    lang,
+    uri: `/${lang}`,
+  };
+
+  res.render('index', data);
+
+});
+
+
+
+router.get('/:lang/category/:tag?', async (req, res, next) => {
+  let lang = ensureLanguage(req.params.lang);
+  let tag = req.params.tag;
+  if (!tag) tag = 'latest';
+
+  let query = {
+    page: req.query.page,
+    length: req.query.length,
+    orderBy: 'publishTime',
+    order: 'desc',
+    base: { status: 9000 },
+    filters: tag == 'latest' ? {} : { tags: tag },
+  }
+
+  let result = await getListBuilder(query, models.Art);
+  organizeData(result.data);
+
+  let data = {
+    title: 'Coloring Page Categories',
+    data: result.data,
+    page: result.page,
+    search: req.query.search,
+    length: result.length,
+    recordsFiltered: result.recordsFiltered,
+    recordsTotal: result.recordsTotal,
+    translate,
+    categories,
+    languages,
+    lang,
+    tag,
+    uri: `/${lang}/category/${tag}`,
+  };
+
+  res.render('category', data);
+
+});
+
+router.get('/:lang/tag/:tag?', async (req, res, next) => {
+  let lang = ensureLanguage(req.params.lang);
+  let tag = req.params.tag;
+  if (!tag) tag = 'latest';
+
+  let query = {
+    page: req.query.page,
+    length: req.query.length,
+    search: req.query.search,
+    orderBy: 'publishTime',
+    order: 'desc',
+    base: { status: 9000 },
+    filters: tag == 'latest' ? {} : { tags: tag },
+  }
+
+  let result = await getListBuilder(query, models.Art);
+  organizeData(result.data);
+
+  let data = {
+    title: 'Coloring Page Tags',
+    data: result.data,
+    page: result.page,
+    length: result.length,
+    recordsFiltered: result.recordsFiltered,
+    recordsTotal: result.recordsTotal,
+    translate,
+    categories,
+    languages,
+    lang,
+    tag,
+    tags,
+    uri: `/${lang}/tag/${tag}`,
+  };
+
+  res.render('tag', data);
+
+});
+
+
+router.get('/:lang/search', async (req, res, next) => {
+  let lang = ensureLanguage(req.params.lang);
+  let search = req.query.search;
+
+
+  let query = {
+    page: req.query.page,
+    length: req.query.length,
+    search: req.query.search,
+    orderBy: 'publishTime',
+    order: 'desc',
+    base: { status: 9000 },
+    filters: {},
+  }
+
+  let result = await getListBuilder(query, models.Art);
+  organizeData(result.data);
+
+  let data = {
+    title: 'Coloring Page Search',
+    data: result.data,
+    page: result.page,
+    length: result.length,
+    recordsFiltered: result.recordsFiltered,
+    recordsTotal: result.recordsTotal,
+    translate,
+    categories,
+    languages,
+    lang,
+    uri: `/${lang}/search?search=${search}`,
+  };
+
+  res.render('search', data);
+
+});
+
+router.get('/:lang/special', async (req, res, next) => {
+  let lang = ensureLanguage(req.params.lang);
+
+  let query = {
+    page: req.query.page,
+    length: req.query.length,
+    orderBy: 'publishTime',
+    order: 'desc',
+    base: { status: 9000 },
+    filters: { hasSpecial: true },
+  }
+
+  let result = await getListBuilder(query, models.Art);
+  organizeData(result.data);
+
+  let data = {
+    title: 'Special Coloring Page',
+    data: result.data,
+    page: result.page,
+    length: result.length,
+    recordsFiltered: result.recordsFiltered,
+    recordsTotal: result.recordsTotal,
+    translate,
+    languages,
+    lang,
+    uri: `/${lang}/special`,
+  };
+
+  res.render('special', data);
+
+});
+
+router.get('/:lang/albums', async (req, res, next) => {
+  let lang = ensureLanguage(req.params.lang);
+
+  // 专辑
+  let albums = await models.ArtAlbum
+    .find({ pid: 'art', enabled: true })
+    .sort({ order: 'asc' })
+    .populate('title')
+    .populate('slogon')
+    .select('tag title slogon contents')
+    .lean()
+    .exec();
+
+
+  for (let doc of albums) {
+    doc.icon = `${config.resHost}/res/coloring/album_icon/320/${doc._id}.jpeg`;
+    doc.cover = `${config.resHost}/res/coloring/album_cover/640/${doc._id}.jpeg`;
+    doc.title = doc.title ? doc.title[lang] : '';
+    doc.slogon = doc.slogon ? doc.slogon[lang] : '';
+    doc.size = doc.contents.length;
+  }
+
+  let data = {
+    title: 'Coloring Page Albums',
+    data: albums,
+    length: albums.length,
+    translate,
+    languages,
+    lang,
+    uri: `/${lang}/albums`,
+  };
+
+  res.render('albums', data);
+
+});
+
+
+router.get('/:lang/album/:id', async (req, res, next) => {
+  let lang = ensureLanguage(req.params.lang);
+  let id = req.params.id;
+
+  // 专辑
+  let doc = await models.ArtAlbum
+    .findById(id)
+    .populate('title')
+    .populate('slogon')
+    .populate({ path: 'contents', select: "width height date publishTime tags lastMod mystery hasSpecial useSpecialThumb publishVersion lock pageId" })
+    .select('tag title slogon contents')
+    .lean()
+    .exec();
+
+  if (!doc) throw createError(404, 'Album Not Found!');
+
+  doc.icon = `${config.resHost}/res/coloring/album_icon/320/${doc._id}.jpeg`;
+  doc.cover = `${config.resHost}/res/coloring/album_cover/640/${doc._id}.jpeg`;
+  doc.title = doc.title ? doc.title[lang] : '';
+  doc.slogon = doc.slogon ? doc.slogon[lang] : '';
+  doc.size = doc.contents.length;
+
+  organizeData(doc.contents);
+
+
+  let data = {
+    title: `${doc.title}`,
+    data: doc,
+    translate,
+    languages,
+    lang,
+    uri: `/${lang}/album/${id}`,
+  };
+
+  res.render('album', data);
+
+});
+
+
+
+router.get('/:lang/detail/:id', async (req, res) => {
+  let lang = ensureLanguage(req.params.lang);
+  let id = req.params.id;
+
+  let doc = await models.Art
+    .findById(id)
+    .select('width height date publishTime tags lastMod mystery hasSpecial useSpecialThumb publishVersion lock pageId desc name')
+    .populate('user', 'username name')
+    .lean()
+    .exec();
+  if (!doc) throw createError(404, 'Art Not Found!');
+
+  organizeDetail(doc, lang);
+
+  // find relate
+  // 算法: 排除掉主流的tag,用剩下的tag去检索,取最多12条记录
+  let tags = [...doc.tags];
+  let cates = categories.map(e => e.id);
+  tags = tags.filter(e => !cates.includes(e));
+  let baseSort = { publishTime: 'desc' };
+
+  let relates = await models.Art
+    .find({ tags: { $in: tags }, status: 9000 })
+    .select('width height date publishTime tags lastMod mystery hasSpecial useSpecialThumb publishVersion lock pageId')
+    .sort(baseSort)
+    .limit(12)
+    .lean()
+    .exec();
+  organizeData(relates);
+
+
+  let data = {
+    title: `Coloring Page ${doc.name}`,
+    data: doc,
+    translate,
+    lang,
+    languages,
+    relates,
+    uri: `/${lang}/detail/${id}`,
+  };
+
+  res.render('detail', data);
+
+});
+
+
+router.get('/play/:id', async (req, res) => {
+  let id = req.params.id;
+  let data = { id };
+  let doc = await models.Art.findById(id);
+  if (!doc) throw createError(404, 'Art Not Found!');
+
+  res.render('play', data);
+});
+
+
+const organizeData = (data) => {
+  data.forEach(doc => {
+    let host = config.resHost;
+    let publishVersion = doc.publishVersion || 0;
+    let version = publishVersion + 1500;
+
+    doc.thumb = `${host}/thumbs/v2/page/480/${doc._id}.jpeg`;
+    if (doc.mystery) { // 神秘图固定用一张
+      doc.thumb = `${host}/thumbs/v2/mystery/480/${doc._id}.jpeg`;
+    } else if (doc.hasSpecial) { // special图有切线图、渐变切线图、灰度图、上传图3中可选
+      let str = 'special_outline';
+      if (doc.useSpecialThumb == 1) {
+        str = 'special_gray';
+      } else if (doc.useSpecialThumb == 2) {
+        str = 'special_thumb';
+      } else if (doc.useSpecialThumb == 3) {
+        str = 'special_gradient';
+      }
+      doc.thumb = `${host}/thumbs/v2/${str}/480/${doc._id}.jpeg`;
+    }
+
+    doc.zip = `${host}/zips/v2/number_mini/${version}/${doc._id}.zip`
+
+    delete doc.hasSpecial;
+    delete doc.useSpecialThumb;
+    delete doc.publishVersion;
+    delete doc.pageId;
+  })
+}
+
+
+const organizeDetail = (doc, lang) => {
+  let host = config.resHost;
+  let publishVersion = doc.publishVersion || 0;
+  let version = publishVersion + 1500;
+
+  doc.thumb = `${host}/thumbs/v2/page/480/${doc._id}.jpeg`;
+  if (doc.mystery) { // 神秘图固定用一张
+    doc.thumb = `${host}/thumbs/v2/mystery/480/${doc._id}.jpeg`;
+  } else if (doc.hasSpecial) { // special图有切线图、渐变切线图、灰度图、上传图3中可选
+    let str = 'special_outline';
+    if (doc.useSpecialThumb == 1) {
+      str = 'special_gray';
+    } else if (doc.useSpecialThumb == 2) {
+      str = 'special_thumb';
+    } else if (doc.useSpecialThumb == 3) {
+      str = 'special_gradient';
+    }
+    doc.thumb = `${host}/thumbs/v2/${str}/480/${doc._id}.jpeg`;
+  }
+
+  doc.zip = `${host}/zips/v2/number_mini/${version}/${doc._id}.zip`
+
+  doc.desc = translate.descTest[lang];
+  doc.title = translate.titleTest[lang];
+  doc.publishTime = format(new Date(doc.publishTime), 'yyyy/MM/dd');
+
+  delete doc.hasSpecial;
+  delete doc.useSpecialThumb;
+  delete doc.publishVersion;
+  delete doc.pageId;
+
+}
+
+
+
+module.exports = router;

+ 56 - 0
routes/proxy.js

@@ -0,0 +1,56 @@
+var express = require('express');
+var router = express.Router();
+const fetch = require('node-fetch');
+
+// 代理请求的中间件
+router.use('/', async (req, res) => {
+  const targetUrl = `http://color.jccytech.cn${req.url}`; // 替换为目标服务器的 URL
+  const method = req.method;
+  const headers = {
+    ...req.headers,
+    // 可能需要根据目标服务器的要求调整或添加请求头
+  };
+
+  // 如果请求包含请求体(例如 POST 请求),则读取它
+  let body;
+  if (req.method === 'POST' || req.method === 'PUT') {
+    body = await getRawBody(req); // 使用 getRawBody 函数读取请求体
+  }
+
+  try {
+    const response = await fetch(targetUrl, {
+      method,
+      headers,
+      body,
+    });
+
+    // 在这里添加 CORS 响应头
+    res.setHeader('Access-Control-Allow-Origin', '*');
+    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
+    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
+
+    // 将目标服务器的响应转发给客户端
+    const responseData = await response.buffer(); // 获取响应的 Buffer
+    res.status(response.status).send(responseData);
+  } catch (error) {
+    // 处理错误,并将错误信息返回给客户端
+    console.error('Proxy error:', error);
+    res.status(500).send('Internal Server Error');
+  }
+});
+
+// 辅助函数:读取请求体的原始数据
+async function getRawBody(req) {
+  return new Promise((resolve, reject) => {
+    let chunks = [];
+    req.on('data', (chunk) => {
+      chunks.push(chunk);
+    });
+    req.on('end', () => {
+      resolve(Buffer.concat(chunks));
+    });
+    req.on('error', reject);
+  });
+}
+
+module.exports = router;

+ 14 - 0
test/script.js

@@ -0,0 +1,14 @@
+// scripts.js
+document.addEventListener('DOMContentLoaded', function () {
+  var menuItems = document.querySelectorAll('.menu-item');
+
+  menuItems.forEach(function (item) {
+    item.addEventListener('mouseover', function () {
+      this.querySelector('.submenu').style.display = 'block';
+    });
+
+    item.addEventListener('mouseout', function () {
+      this.querySelector('.submenu').style.display = 'none';
+    });
+  });
+});

+ 71 - 0
test/styles.css

@@ -0,0 +1,71 @@
+/* styles.css */
+body {
+  font-family: Arial, sans-serif;
+}
+
+nav {
+  background-color: #333;
+}
+
+.menu {
+  list-style: none;
+  padding: 0;
+  margin: 0;
+  display: flex;
+}
+
+.menu-item {
+  position: relative;
+}
+
+.menu-item > a {
+  display: block;
+  padding: 15px 20px;
+  color: white;
+  text-decoration: none;
+  background-color: #333;
+  transition: background-color 0.3s;
+}
+
+.menu-item > a:hover {
+  background-color: #575757;
+}
+
+.submenu {
+  list-style: none;
+  padding: 0;
+  margin: 0;
+  display: none;
+  position: absolute;
+  top: 100%;
+  left: 0;
+  background-color: #444;
+  min-width: 200px;
+  z-index: 1;
+}
+
+.submenu li {
+  border-bottom: 1px solid #555;
+}
+
+.submenu li:last-child {
+  border-bottom: none;
+}
+
+.submenu a {
+  padding: 10px 20px;
+  color: white;
+  text-decoration: none;
+  display: block;
+  background-color: #444;
+  transition: background-color 0.3s;
+}
+
+.submenu a:hover {
+  background-color: #555;
+}
+
+/* 当鼠标悬停在菜单项上时显示子菜单 */
+.menu-item:hover .submenu {
+  display: block;
+}

+ 187 - 0
test/tags.html

@@ -0,0 +1,187 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <style>
+       .tag-cloud {
+          padding: 20px;
+        }
+
+       .tag {
+            display: inline-block;
+            padding: 5px 10px;
+            margin: 5px;
+            border-radius: 5px;
+            cursor: pointer;
+            background-color: #fefefe;
+            transition: color 0.3s ease;
+        }
+
+        .tag:hover {
+            transform: scale(2.0); /* 悬停时轻微放大 */
+        }
+    </style>
+</head>
+
+<body>
+    <div class="tag-cloud">
+        <!-- 这里根据提供的tags数组生成标签 -->
+        <span class="tag" style="color: #FF5733;">animal</span>
+        <span class="tag" style="color: #33FF57;">scenery</span>
+        <span class="tag" style="color: #5733FF;">people</span>
+        <span class="tag" style="color: #FF33E7;">life</span>
+        <span class="tag" style="color: #33E7FF;">plant</span>
+        <span class="tag" style="color: #E7FF33;">girl</span>
+        <span class="tag" style="color: #FF3385;">flower</span>
+        <span class="tag" style="color: #3385FF;">fantasy</span>
+        <span class="tag" style="color: #85FF33;">data_good</span>
+        <span class="tag" style="color: #FF8533;">mandala</span>
+        <span class="tag" style="color: #33FF85;">food</span>
+        <span class="tag" style="color: #8533FF;">special_date</span>
+        <span class="tag" style="color: #FF3357;">nature</span>
+        <span class="tag" style="color: #3357FF;">cat</span>
+        <span class="tag" style="color: #57FF33;">landscape</span>
+        <span class="tag" style="color: #FF5785;">dog</span>
+        <span class="tag" style="color: #853357;">countryside</span>
+        <span class="tag" style="color: #5785FF;">forest</span>
+        <span class="tag" style="color: #FF8585;">ban</span>
+        <span class="tag" style="color: #85FF85;">flowers</span>
+        <span class="tag" style="color: #8585FF;">bird</span>
+        <span class="tag" style="color: #FF85E7;">river</span>
+        <span class="tag" style="color: #E785FF;">mountains</span>
+        <span class="tag" style="color: #85E7FF;">snow</span>
+        <span class="tag" style="color: #E7FF85;">recommend</span>
+        <span class="tag" style="color: #FFE733;">winter</span>
+        <span class="tag" style="color: #33FFE7;">house</span>
+        <span class="tag" style="color: #E733FF;">village</span>
+        <span class="tag" style="color: #FFE785;">heart</span>
+        <span class="tag" style="color: #85E733;">Christmas</span>
+        <span class="tag" style="color: #3385E7;">garden</span>
+        <span class="tag" style="color: #E73385;">butterfly</span>
+        <span class="tag" style="color: #8533E7;">fashion</span>
+        <span class="tag" style="color: #E78585;">summer</span>
+        <span class="tag" style="color: #85E785;">farm</span>
+        <span class="tag" style="color: #8585E7;">boy</span>
+        <span class="tag" style="color: #E78533;">place</span>
+        <span class="tag" style="color: #33E785;">sea</span>
+        <span class="tag" style="color: #E73357;">culture</span>
+        <span class="tag" style="color: #57E733;">car</span>
+        <span class="tag" style="color: #3357E7;">horse</span>
+        <span class="tag" style="color: #5733E7;">lake</span>
+        <span class="tag" style="color: #E75733;">autumn</span>
+        <span class="tag" style="color: #33E757;">tree</span>
+        <span class="tag" style="color: #E75785;">building</span>
+        <span class="tag" style="color: #85E757;">wild</span>
+        <span class="tag" style="color: #5785E7;">woman</span>
+        <span class="tag" style="color: #E757E7;">room</span>
+        <span class="tag" style="color: #E7E733;">park</span>
+        <span class="tag" style="color: #33E7E7;">ocean</span>
+        <span class="tag" style="color: #E733E7;">meadow</span>
+        <span class="tag" style="color: #E7E785;">rabbit</span>
+        <span class="tag" style="color: #85E7E7;">family</span>
+        <span class="tag" style="color: #E785E7;">patterns</span>
+        <span class="tag" style="color: #E7E757;">home</span>
+        <span class="tag" style="color: #57E7E7;">mountain</span>
+        <span class="tag" style="color: #E75733;">halloween</span>
+        <span class="tag" style="color: #33E757;">bridge</span>
+        <span class="tag" style="color: #E75785;">friends</span>
+        <span class="tag" style="color: #85E757;">city</span>
+        <span class="tag" style="color: #5785E7;">baby</span>
+        <span class="tag" style="color: #E757E7;">sunset</span>
+        <span class="tag" style="color: #E7E733;">simple</span>
+        <span class="tag" style="color: #33E7E7;">boat</span>
+        <span class="tag" style="color: #E733E7;">window</span>
+        <span class="tag" style="color: #E7E785;">plants</span>
+        <span class="tag" style="color: #85E7E7;">man</span>
+        <span class="tag" style="color: #E785E7;">trees</span>
+        <span class="tag" style="color: #E7E757;">fruit</span>
+        <span class="tag" style="color: #57E7E7;">rose</span>
+        <span class="tag" style="color: #E75733;">vacation</span>
+        <span class="tag" style="color: #33E757;">evening</span>
+        <span class="tag" style="color: #E75785;">castle</span>
+        <span class="tag" style="color: #85E757;">snowman</span>
+        <span class="tag" style="color: #5785E7;">street</span>
+        <span class="tag" style="color: #E757E7;">tiger</span>
+        <span class="tag" style="color: #E7E733;">grass</span>
+        <span class="tag" style="color: #33E7E7;">lady</span>
+        <span class="tag" style="color: #E733E7;">child</span>
+        <span class="tag" style="color: #E7E785;">vintage</span>
+        <span class="tag" style="color: #85E7E7;">holiday</span>
+        <span class="tag" style="color: #E785E7;">pasture</span>
+        <span class="tag" style="color: #E7E757;">deer</span>
+        <span class="tag" style="color: #57E7E7;">sweet</span>
+        <span class="tag" style="color: #E75733;">night</span>
+        <span class="tag" style="color: #33E757;">beach</span>
+        <span class="tag" style="color: #E75785;">travel</span>
+        <span class="tag" style="color: #85E757;">dress</span>
+        <span class="tag" style="color: #5785E7;">fish</span>
+        <span class="tag" style="color: #E757E7;">couple</span>
+        <span class="tag" style="color: #E7E733;">view</span>
+        <span class="tag" style="color: #33E7E7;">fairy</span>
+        <span class="tag" style="color: #E733E7;">harvest</span>
+        <span class="tag" style="color: #E7E785;">yard</span>
+        <span class="tag" style="color: #85E7E7;">sky</span>
+        <span class="tag" style="color: #E785E7;">toys</span>
+        <span class="tag" style="color: #E7E757;">wildflowers</span>
+        <span class="tag" style="color: #57E7E7;">love</span>
+        <span class="tag" style="color: #E75733;">pumpkin</span>
+        <span class="tag" style="color: #33E757;">water</span>
+        <span class="tag" style="color: #E75785;">transportation</span>
+        <span class="tag" style="color: #85E757;">birds</span>
+        <span class="tag" style="color: #5785E7;">rocks</span>
+        <span class="tag" style="color: #E757E7;">famous</span>
+        <span class="tag" style="color: #E7E733;">downtown</span>
+        <span class="tag" style="color: #33E7E7;">ice</span>
+        <span class="tag" style="color: #E733E7;">bear</span>
+        <span class="tag" style="color: #E7E785;">spring</span>
+        <span class="tag" style="color: #85E7E7;">road</span>
+        <span class="tag" style="color: #E785E7;">wolf</span>
+        <span class="tag" style="color: #E7E757;">unicorn</span>
+        <span class="tag" style="color: #57E7E7;">lion</span>
+        <span class="tag" style="color: #E75733;">stone</span>
+        <span class="tag" style="color: #33E757;">magic</span>
+        <span class="tag" style="color: #E75785;">cake</span>
+        <span class="tag" style="color: #85E757;">book</span>
+        <span class="tag" style="color: #5785E7;">furniture</span>
+        <span class="tag" style="color: #E757E7;">children</span>
+        <span class="tag" style="color: #E7E733;">fox</span>
+        <span class="tag" style="color: #33E7E7;">zhy03re</span>
+        <span class="tag" style="color: #E733E7;">cartoon</span>
+        <span class="tag" style="color: #E7E785;">duck</span>
+        <span class="tag" style="color: #85E7E7;">owl</span>
+        <span class="tag" style="color: #E785E7;">squirrel</span>
+        <span class="tag" style="color: #E7E757;">sport</span>
+        <span class="tag" style="color: #57E7E7;">angel</span>
+        <span class="tag" style="color: #E75733;">cub</span>
+        <span class="tag" style="color: #33E757;">decorations</span>
+        <span class="tag" style="color: #E75785;">pond</span>
+        <span class="tag" style="color: #85E757;">sheep</span>
+        <span class="tag" style="color: #5785E7;">fence</span>
+        <span class="tag" style="color: #E757E7;">interior</span>
+        <span class="tag" style="color: #E7E733;">coastline</span>
+        <span class="tag" style="color: #33E7E7;">beauty</span>
+        <span class="tag" style="color: #E733E7;">chicken</span>
+        <span class="tag" style="color: #E7E785;">train</span>
+        <span class="tag" style="color: #85E7E7;">mermaid</span>
+        <span class="tag" style="color: #E785E7;">history</span>
+        <span class="tag" style="color: #E7E757;">moon</span>
+        <span class="tag" style="color: #57E7E7;">picnic</span>
+        <span class="tag" style="color: #E75733;">bedroom</span>
+        <span class="tag" style="color: #33E757;">blooming</span>
+        <span class="tag" style="color: #E75785;">sunflower</span>
+        <span class="tag" style="color: #85E757;">mystery</span>
+        <span class="tag" style="color: #5785E7;">lovers</span>
+        <span class="tag" style="color: #E757E7;">stairs</span>
+        <span class="tag" style="color: #E7E733;">walk</span>
+        <span class="tag" style="color: #33E7E7;">jungle</span>
+        <span class="tag" style="color: #E733E7;">livingroom</span>
+        <span class="tag" style="color: #E7E785;">thanksgiving</span>
+        <span class="tag" style="color: #85E7E7;">kingdom</span>
+        <span class="tag" style="color: #E785E7;">mother and daughter</span>
+        <span class="tag" style="color: #E7E757;">domestic</span>
+        <span class="tag" style="color: #57E7E7;">mother</span>
+        <span class="tag" style="color: #E75733;">lotus</span>
+  </div>
+  </body>

+ 29 - 0
test/test.html

@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>下拉菜单示例</title>
+    <link rel="stylesheet" href="styles.css">
+</head>
+<body>
+    <nav>
+        <ul class="menu">
+            <li class="menu-item" id="menu-home">
+                <a href="#">首页</a>
+                <ul class="submenu" id="submenu-home">
+                    <li><a href="#">分类</a></li>
+                    <li><a href="#">专辑</a></li>
+                    <li><a href="#">彩绘专区</a></li>
+                    <li><a href="#">关于</a></li>
+                    <li><a href="#">APP下载</a></li>
+                    <li><a href="#">联系我们</a></li>
+                    <li><a href="#">意见反馈</a></li>
+                </ul>
+            </li>
+        </ul>
+    </nav>
+
+    <script src="scripts.js"></script>
+</body>
+</html>

+ 19 - 0
views/album-section.ejs

@@ -0,0 +1,19 @@
+
+<div class="content-wrapper">
+  <div class="content-title">
+    <div style="font-size: 20px; font-weight: bold;"><%= translate.selectAlbums[lang] %>:</div>
+    <a href="<%= lang %>/albums"><%= translate.more[lang] %> >>></a>
+  </div>
+
+  <div class="content">
+    <div class="album-icon-grid">
+        <% albums.forEach(album => { %>
+          <div class="album-grid-card">
+            <a href="/<%= lang %>/album/<%= album._id %>"><img src="<%= album.icon %>" alt="<%= album.title %>" ></a>
+            <div style="padding: 0px 0px 4px 4px"><%= album.title %></div>
+          </div>
+        <% }); %>
+    </div>
+  </div>
+
+</div>

+ 32 - 0
views/album.ejs

@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <%- include('head') %>
+  <link rel="stylesheet" href="/stylesheets/album.css">
+</head>
+
+<body>  
+  <%- include('header') %>
+
+  <div class="album-header">
+    <h1 style="color: purple"><%= data.title %></h1>
+    <img style="border-radius: 8px;" src="<%= data.cover %>" alt="<%= data.title %>">
+    <div style="color: gray; font-size: 18px; padding: 10px"><%= data.slogon %></div>
+  </div>
+
+  <div class="content">
+    <div class="image-grid">
+        <% data.contents.forEach(item => { %>
+            <a href="/<%= lang %>/detail/<%= item._id %>"><img src="<%= item.thumb %>" alt="<%= item.name %>" ></a>
+        <% }); %>
+    </div>
+  </div>
+
+  <div style="padding: 40px"></div>
+
+
+</body>
+</html>
+
+
+

+ 28 - 0
views/albums.ejs

@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <%- include('head') %>
+</head>
+
+<body>  
+  <%- include('header') %>
+
+  <h1 style="display: flex; justify-content: center; padding: 10px; color: purple"><%= translate.allAlbums[lang] %></h1>
+
+  <div class="content">
+    <div class="album-grid">
+        <% data.forEach(album => { %>
+          <div class="album-grid-card">
+            <a href="/<%= lang %>/album/<%= album._id %>"><img src="<%= album.cover %>" alt="<%= album.title %>" ></a>
+            <div style="padding: 0px 0px 4px 4px"><%= album.title %></div>
+          </div>
+        <% }); %>
+    </div>
+  </div>
+
+
+</body>
+</html>
+
+
+

+ 69 - 0
views/banner.ejs

@@ -0,0 +1,69 @@
+<link rel="stylesheet" href="/stylesheets/banner.css">
+
+<div class="carousel-container">
+  <button class="btn-prev">❮</button>
+  <div class="carousel-images">
+    <div class="item" style="display: flex; flex-direction: row; justify-content: center; align-items: center;">
+      <img width="50%" src="http://color.jccytech.cn/thumbs/v2/page/480/5d8898684278e31afd9b6968.jpeg" alt="<%= translate.cuteCat[lang] %>">
+      <div style="padding-right: 60px;">
+        <h1><%= translate.cuteCat[lang] %></h1>
+        <p class="cat-description"><%= translate.cuteCatDescription[lang] %></p>
+        <a href="/play/5d8898684278e31afd9b6968" class="carousel-play-btn"><%= translate.play[lang] %></a>
+      </div>
+    </div>
+
+    <div class="item" style="display: flex; flex-direction: row; justify-content: space-evenly; align-items: center;">
+      <div>
+        <h1><%= translate.daily[lang] %></h1>
+        <div style="font-weight: bold; font-size: 20px; color: #aaa; padding-bottom: 20px;">2025/01/19</div>
+        <a href="/play/6783978993b021143d362277" class="carousel-play-btn"><%= translate.play[lang] %></a>
+      </div>
+      <img width="40%" src="http://color.jccytech.cn/thumbs/v2/page/480/6783978993b021143d362277.jpeg" alt="<%= translate.daily[lang] %>">
+    </div>
+
+    <div class="item" style="position: relative; display: inline-block;">
+      <img width="100%" src="https://color.jccytech.cn/res/coloring/album_cover/720/661cf6deaae27d6dda120bcc.jpeg" alt="<%= translate.cuteKids[lang] %>">
+      <h3 style="position: absolute; top: 20px; left: 20px; color:#ccc; "><%= translate.album[lang] %></h3>
+      <h1 style="position: absolute; top: 40px; left: 20px; color:white; "><%= translate.cuteKids[lang] %></h1>
+    </div>
+
+    <div class="item" style="position: relative; display: inline-block;">
+      <img width="100%" src="https://color.jccytech.cn/res/coloring/album_cover/720/670e26713562c348e1b64db0.jpeg" alt="<%= translate.animalsHappyTime[lang] %>">
+      <h3 style="position: absolute; top: 20px; left: 20px; color:#ccc; "><%= translate.album[lang] %></h3>
+      <h1 style="position: absolute; top: 40px; left: 20px; color:white; "><%= translate.animalsHappyTime[lang] %></h1>
+    </div>
+
+  </div>
+  <button class="btn-next">❯</button>
+</div>
+
+<script>
+  document.addEventListener('DOMContentLoaded', function () {
+    const carouselImages = document.querySelector('.carousel-images');
+    const items = document.querySelectorAll('.carousel-images .item');
+    const prevButton = document.querySelector('.btn-prev');
+    const nextButton = document.querySelector('.btn-next');
+    let currentIndex = 0;
+
+    function showItem(index) {
+      if (index >= items.length) {
+        currentIndex = 0;
+      } else if (index < 0) {
+        currentIndex = items.length - 1;
+      } else {
+        currentIndex = index;
+      }
+      const offset = -currentIndex * 100; // 每张图片占100%宽度
+      carouselImages.style.transform = `translateX(${offset}%)`;
+    }
+
+    prevButton.addEventListener('click', function () {
+      showItem(currentIndex - 1);
+    });
+
+    nextButton.addEventListener('click', function () {
+      showItem(currentIndex + 1);
+    });
+  });
+
+</script>

+ 31 - 0
views/category.ejs

@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <%- include('head') %>
+  <link rel="stylesheet" href="/stylesheets/category.css">
+</head>
+
+<body>
+  <%- include('header') %>
+  <div class="category">  
+    <% categories.forEach(item => { %>
+      <a href="/<%= lang %>/category/<%= item.id %>" class="<%= tag == item.id ? 'selected' : '' %>"><%= item[lang] %></a>
+    <% }); %>
+    <a href="/<%= lang %>/tag" style="color: #ff4081"><%= translate.more[lang] %>>>></a>
+  </div>
+  
+  <div class="content">
+    <div class="image-grid">
+        <% data.forEach(item => { %>
+            <a href="/<%= lang %>/detail/<%= item._id %>"><img src="<%= item.thumb %>" alt="<%= item.name %>" ></a>
+        <% }); %>
+    </div>
+  </div>
+
+  <%- include('pagination') %>
+
+</body>
+</html>
+
+
+

+ 39 - 0
views/detail.ejs

@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <%- include('head') %>
+  <link rel="stylesheet" href="/stylesheets/detail.css">
+</head>
+
+<body>
+    <%- include('header') %>
+    <div class="details">
+        <div class="poster">
+            <img src="<%= data.thumb %>" alt="<%= data.title %>">
+        </div>
+        <div class="description">
+            <div style="font-size: 30px; font-weight: 700;"><%= data.title %></div>
+            <p><%= translate.designer[lang] %>: <%= data.user.username %></p>
+            <p><%= translate.publishTime[lang] %>: <%= data.publishTime %></p>
+            <p><%= translate.tag[lang] %>:
+                <% data.tags.forEach(tag => { %>
+                    <a href="/<%= lang %>/tag/<%= tag %>" class="tag-button"><%= tag %></a>
+                <% }); %>
+            </p>
+            <div><%= data.desc %></div>
+            <a href="/play/<%= data._id %>" class="play-button"><%= translate.play[lang] %></a>
+        </div>
+    </div>
+
+    <p style="display: flex; justify-content: center; color: #777; font-size: 18px; font-weight: 500;">猜您喜欢:<p>
+
+    <div class="content" style="margin-bottom: 40px;">
+        <div class="image-grid">
+            <% relates.forEach(item => { %>
+                <a href="/<%= lang %>/detail/<%= item._id %>"><img src="<%= item.thumb %>" alt="<%= item.name %>" ></a>
+            <% }); %>
+        </div>
+    </div>
+
+</body>
+</html>

+ 29 - 0
views/footer.ejs

@@ -0,0 +1,29 @@
+<link rel="stylesheet" href="/stylesheets/footer.css">
+<footer>
+  <!-- <div class="footer-content">
+    <div class="footer-section">
+        <h3>关于Art Number Coloring</h3>
+        <p>Art Number Coloring是一款专为艺术爱好者设计的数字涂色游戏应用,拥有海量的线条底图资源,并且完全免费。精美的构图,多彩的颜色,绝佳的游戏体验,在感受色彩艺术气息的同时,为您的休闲娱乐生活带来意想不到的愉悦。</p>
+    </div>
+    <div class="footer-section">
+        <h3>App下载</h3>
+        <ul>
+            <li><a href="#">iOS下载</a></li>
+            <li><a href="#">Android下载</a></li>
+        </ul>
+    </div>
+    <div class="footer-section">
+        <h3>联系我们</h3>
+        <p>邮箱: contact@artnumbercoloring.com</p>
+        <p>电话: +8613530139503</p>
+    </div>
+    <div class="footer-section">
+        <h3>意见反馈</h3>
+        <p>我们非常重视您的意见,请点击<a href="#">这里</a>进行反馈。</p>
+    </div>
+  </div> -->
+  <div class="footer-bottom">
+      <p>Copyright &copy; 2025 Art Number Coloring All Rights Reserved</p>
+  </div>
+</footer>
+

+ 5 - 0
views/head.ejs

@@ -0,0 +1,5 @@
+<meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title><%= title %></title>
+  <link rel="icon" href="/assets/icon/favicon.ico" type="image/x-icon">
+  <link rel="stylesheet" href="/stylesheets/styles.css">

+ 56 - 0
views/header.ejs

@@ -0,0 +1,56 @@
+<link rel="stylesheet" href="/stylesheets/header.css">
+
+<header class="header">
+  <div class="dropdown">
+    <svg class="dropbtn" style="margin-right: 5px;" width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+      <path opacity="0.5" d="M21 6L3 6" stroke="#1C274C" stroke-width="2.0" stroke-linecap="round"/>
+      <path opacity="0.5" d="M21 10L3 10" stroke="#1C274C" stroke-width="2.0" stroke-linecap="round"/>
+      <path opacity="0.5" d="M10 14H3" stroke="#1C274C" stroke-width="2.0" stroke-linecap="round"/>
+      <path opacity="0.5" d="M10 18H3" stroke="#1C274C" stroke-width="2.0" stroke-linecap="round"/>
+      <path d="M14 15L17.5 18L21 15" stroke="#1C274C" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
+      </svg>
+    <div class="dropdown-home-content">
+      <a href="/<%= lang %>" class="<%= uri == `/${lang}` ? 'selected' : '' %>"><%= translate.homePage[lang] %></a>
+      <a href="/<%= lang %>/category" class="<%= uri.includes(`/${lang}/category`) ? 'selected' : '' %>"><%= translate.categoryPage[lang] %></a>
+      <a href="/<%= lang %>/tag" class="<%= uri.includes(`/${lang}/tag`) ? 'selected' : '' %>"><%= translate.tagPage[lang] %></a>
+      <a href="/<%= lang %>/albums" class="<%= uri.includes(`/${lang}/albums`) ? 'selected' : '' %>"><%= translate.album[lang] %></a>
+      <a href="/<%= lang %>/special" class="<%= uri.includes(`/${lang}/special`) ? 'selected' : '' %>"><%= translate.special[lang] %></a>
+      <div class="divider"></div>
+      <a href="/<%= lang %>/about"><%= translate.about[lang] %></a>
+      <a href="/<%= lang %>/contact-us"><%= translate.contactUs[lang] %></a>
+      <a href="/<%= lang %>/feedback"><%= translate.feedback[lang] %></a>
+      <div class="divider"></div>
+      <p class="copyright">Copyright &copy; 2025 Art Number Coloring All Rights Reserved</p>
+    </div>
+  </div>
+
+  <a href="/<%= lang %>"><img src="/assets/svg/logo.svg", alt="Art Number Coloring"></a>
+
+  <div class="search-container">
+    <form action="/<%= lang %>/search" method="GET" class="search-box">
+      <input type="text" name="search" placeholder="Search...">
+      <button type="submit">
+          <svg fill="#000000" width="16px" height="16px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+              <path d="M12.027 9.92L16 13.95 14 16l-4.075-3.976A6.465 6.465 0 0 1 6.5 13C2.91 13 0 10.083 0 6.5 0 2.91 2.917 0 6.5 0 10.09 0 13 2.917 13 6.5a6.463 6.463 0 0 1-.973 3.42zM1.997 6.452c0 2.48 2.014 4.5 4.5 4.5 2.48 0 4.5-2.015 4.5-4.5 0-2.48-2.015-4.5-4.5-4.5-2.48 0-4.5 2.014-4.5 4.5z" fill-rule="evenodd"/>
+          </svg>
+      </button>
+    </form>
+  </div>
+
+
+  <div class="header-right">
+
+    <div class="dropdown">
+      <svg class="dropbtn" width="24px" height="24px" viewBox="0 0 24 24" role="img" xmlns="http://www.w3.org/2000/svg" aria-labelledby="languageIconTitle" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none" color="#000000"> <title id="languageIconTitle">Language</title> <circle cx="12" cy="12" r="10"/> <path stroke-linecap="round" d="M12,22 C14.6666667,19.5757576 16,16.2424242 16,12 C16,7.75757576 14.6666667,4.42424242 12,2 C9.33333333,4.42424242 8,7.75757576 8,12 C8,16.2424242 9.33333333,19.5757576 12,22 Z"/> <path stroke-linecap="round" d="M2.5 9L21.5 9M2.5 15L21.5 15"/> </svg>
+      <div class="dropdown-content">
+        <% languages.forEach(lg => { %>
+            <a href="/<%= lg.code %>" class="<%= lg.code == lang ? 'selected' : '' %>" style="font-size: 16px"><%= lg.title %></a>
+        <% }); %>
+      </div>
+    </div>
+
+    <a href="/<%= lang %>/app" class="header-right-btn"><%= translate.app[lang] %></a>
+    <a href="/<%= lang %>/my-works" class="header-right-btn"><%= translate.my[lang] %></a>
+  </div>
+
+</header>

+ 16 - 0
views/hot-section.ejs

@@ -0,0 +1,16 @@
+
+<div class="content-wrapper">
+  <div class="content-title">
+    <div style="font-size: 20px; font-weight: bold;"><%= translate.hot[lang] %>:</div>
+    <a href="/<%= lang %>/category/data_good"><%= translate.more[lang] %>>>></a>
+  </div>
+
+  <div class="content">
+    <div class="image-grid">
+        <% recommend.forEach(item => { %>
+            <a href="/<%= lang %>/detail/<%= item._id %>"><img src="<%= item.thumb %>" alt="<%= item.name %>" ></a>
+        <% }); %>
+    </div>
+  </div>
+
+</div>

+ 15 - 0
views/index.ejs

@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <%- include('head') %>
+</head>
+<body>
+  <%- include('header') %>
+  <%- include('banner') %>
+  <%- include('latest-section') %>
+  <%- include('album-section') %>
+  <%- include('hot-section') %>
+  <%- include('special-section') %>
+  <div style="height: 50px;"></div>
+</body>
+</html>

+ 16 - 0
views/latest-section.ejs

@@ -0,0 +1,16 @@
+
+<div class="content-wrapper">
+  <div class="content-title">
+    <div style="font-size: 20px; font-weight: bold;"><%= translate.latest[lang] %>:</div>
+    <a href="/<%= lang %>/category/latest"><%= translate.more[lang] %>>>></a>
+  </div>
+
+  <div class="content">
+    <div class="image-grid">
+        <% latest.forEach(item => { %>
+            <a href="/<%= lang %>/detail/<%= item._id %>"><img src="<%= item.thumb %>" alt="<%= item.name %>" ></a>
+        <% }); %>
+    </div>
+  </div>
+
+</div>

+ 45 - 0
views/pagination.ejs

@@ -0,0 +1,45 @@
+<link rel="stylesheet" href="/stylesheets/pagination.css">
+<%
+// 计算总页数
+const totalPages = Math.ceil(recordsFiltered / length);
+
+// 确定前后展示的页码范围
+const startPage = Math.max(1, page - 3); // 前面至少展示2个页码(包括当前页的前一页)
+const endPage = Math.min(totalPages, page + 5 - (page % 5 === 0 ? 0 : 1)); // 后面展示页码数根据当前页是否为5的倍数来调整,确保总展示数为5(前面)+1(后面)或6(无省略号时)
+const showEllipsis = totalPages > 6 && (startPage > 1 || endPage < totalPages); // 判断是否需要显示省略号
+%>
+
+<div class="pagination">
+    <span style="font-size: 15px; font-family:sans-serif; margin-right: 20px;"><%= translate.total[lang] %> <%= recordsFiltered %> <%= translate.item[lang] %>,  <%= length %><%= translate.item[lang] %>/<%= translate.page[lang] %></span>
+    <ul>
+        <% for (let i = startPage; i <= endPage; i++) { %>
+            <li class="<%= i === page ? 'active' : '' %>">
+                <a href="<%= uri %><%= uri.includes('?')? '&' : '?' %>page=<%= i %>&length=<%= length %>" class="<%= i === page ? 'active' : '' %>"> <%= i %> </a>
+            </li>
+        <% } %>
+        <% if (showEllipsis) { %>
+            <li class="ellipsis">...</li>
+        <% } %>
+        <% if (endPage < totalPages) { %>
+            <li>
+                <a href="<%= uri %><%= uri.includes('?')? '&' : '?' %>page=<%= totalPages %>&length=<%= length %>"><%= totalPages %></a>
+            </li>
+        <% } %>
+    </ul>
+    <input type="number" id="pageInput" min="1" max="<%= totalPages %>">
+    <button onclick="jumpToPage()"><%= translate.jumpTo[lang] %></button>
+</div>
+
+<script>
+  function jumpToPage() {
+    var pageInput = document.getElementById('pageInput');
+    var page = parseInt(pageInput.value, 10);
+    var totalPages = '<%= totalPages %>'; // 从EJS模板中获取的总页数
+    if (!isNaN(page) && page > 0 && page <= totalPages) {
+        window.location.href = `<%= uri %><%= uri.includes('?')? '&' : '?' %>page=${page}&length=<%= length %>`
+    } else {
+        alert('<%= translate.wrongPage[lang] %> :(1-<%= totalPages %>)');
+    }
+  }
+</script>
+

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません