Explorar o código

1.art增加统计字段;2.detail页面加上分享、打印、app下载; tag页面针对不同tag不同的seo信息

guoziyun hai 1 ano
pai
achega
1a5e266abe

+ 2 - 0
app.js

@@ -73,6 +73,8 @@ app.use('/proxy', require('./routes/proxy'));
 
 app.use('/', require('./routes/index'));
 
+app.use('/download', require('./routes/res/download'));
+
 
 
 // catch 404 and forward to error handler

+ 89 - 38
config/tag.js

@@ -1,3 +1,7 @@
+const models = require('../models');
+const translate = require('../config/translate');
+const meta = require('../config/meta');
+
 let tags = [
   "animal",
   "scenery",
@@ -123,42 +127,42 @@ let tags = [
   "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",
+  "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() {
@@ -177,8 +181,55 @@ function getRandomDarkColor() {
   return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
 }
 
-
 tags = tags.map(e => { return { tag: e, color: getRandomDarkColor() } });
 
 
-module.exports = tags;
+
+let tagSeoMap = {};
+
+async function getTagSeoTitle(tag, lang) {
+  let item = tagSeoMap[tag];
+  if (!item) {
+    let doc = await models.TagSeo.findOne({ tag }).lean();
+    if (doc) {
+      try {
+        doc.seoTitle = JSON.parse(doc.seoTitle);
+        doc.seoDescription = JSON.parse(doc.seoDescription);
+        tagSeoMap[tag] = doc;
+        item = doc;
+      } catch (e) {
+        console.error(e.message);
+      }
+    }
+  }
+  if (item) {
+    return tagSeoMap[tag].seoTitle[lang];
+  } else {
+    return `${tag} coloring pages | ${translate.printableColoringPage[lang]}`;
+  }
+}
+
+async function getTagSeoDescription(tag, lang) {
+  let item = tagSeoMap[tag];
+  if (!item) {
+    let doc = await models.TagSeo.findOne({ tag }).lean();
+    if (doc) {
+      try {
+        doc.seoTitle = JSON.parse(doc.seoTitle);
+        doc.seoDescription = JSON.parse(doc.seoDescription);
+        tagSeoMap[tag] = doc;
+        item = doc;
+      } catch (e) {
+        console.error(e.message);
+      }
+    }
+  }
+  if (item) {
+    return tagSeoMap[tag].seoDescription[lang];
+  } else {
+    return meta.tagDescription[lang];
+  }
+}
+
+
+module.exports = { tags, getTagSeoTitle, getTagSeoDescription };

+ 36 - 10
config/translate.js

@@ -23,11 +23,11 @@ let introTitle = {
 }
 
 let introText = {
-  zh: '是一个专业的填色页网站,拥有数量最多(超过 15000 多页)且类型最为全面的填色页资源。它每日更新,适合大人和小孩。借助我们智能的人工智能搜索功能,你可以找到任何你想要的填色页。你可以下载、打印,或者在线玩填色游戏(按数字填色)。',
-  en: 'is a professional coloring page website with the largest quantity(over 15000+) and the most comprehensive types.  It is updated daily and is suitable for both adults and kids. With our intelligent AI search, you will find any kinds of coloring pages you want. Your can download, print,  or play the game online(paint by number). ',
-  es: 'es un sitio web profesional de páginas de colorear con la mayor cantidad (más de 15.000+) y los tipos más completos. Se actualiza diariamente y es adecuado tanto para adultos como para niños. Con nuestra búsqueda inteligente de IA, encontrarás cualquier tipo de páginas de colorear que desees. Puedes descargarlas, imprimirlas o jugar al juego en línea (colorear por número).',
-  pt: 'é um site profissional de páginas de colorir com a maior quantidade (mais de 15.000+) e os tipos mais completos. É atualizado diariamente e é adequado tanto para adultos quanto para crianças. Com nossa busca inteligente de IA, você encontrará qualquer tipo de páginas de colorir que queira. Você pode baixá-las, imprimi-las ou jogar o jogo online (colorir por número).',
-  ja: 'は、専門の着色用ページのサイトで、最も数が多く(15000 枚以上)、種類も最も充実しています。毎日更新され、大人も子供も利用できます。当社のスマートな AI 検索機能を使えば、あなたが望むどんな種類の着色用ページでも見つけることができます。ダウンロード、印刷、またはオンラインでゲーム(数字で塗る)をすることができます。',
+  zh: '是一个专业的填色页网站,涂画爱好者的乐园。拥有数量最多(超过 15000 多页)且类型最为全面的填色页资源。它每日更新,适合大人和小孩,你可以找到任何你想要的填色页。它们全部都是免费的,你可以下载、打印,分享。此外,它还有在线游戏的功能,可以在线涂色。支持网页端和手机APP。',
+  en: 'A professional coloring page website, a paradise for coloring enthusiasts. It boasts the largest collection (over 15,000 pages) and most comprehensive range of coloring page resources. Updated daily, suitable for both adults and kids. All coloring pages are free for download, printing, and sharing. Additionally, it features online games for digital coloring. It supports both web browsers and mobile apps.',
+  es: 'Un sitio web profesional de páginas para colorear, un paraíso para los entusiastas del coloreado. Cuenta con la colección más grande (más de 15,000 páginas) y la gama más completa de recursos de páginas para colorear. Actualizado diariamente, adecuado tanto para adultos como para niños. Todas las páginas para colorear son gratuitas para descargar, imprimir y compartir. Además, ofrece juegos en línea para colorear digitalmente. Es compatible con navegadores web y aplicaciones móviles.',
+  pt: 'Um site profissional de páginas para colorir, um paraíso para entusiastas da coloração. Possui a maior coleção (mais de 15.000 páginas) e a gama mais abrangente de recursos de páginas para colorir. Atualizado diariamente, adequado tanto para adultos quanto para crianças. Todas as páginas para colorir são gratuitas para download, impressão e compartilhamento. Além disso, oferece jogos online para colorir digitalmente. Suporta navegadores da web e aplicativos móveis.',
+  ja: 'プロの塗り絵ウェブサイト、塗り絵愛好家のための楽園です。最大のコレクション(15,000ページ以上)と最も包括的な塗り絵リソースを誇ります。毎日更新され、大人と子供の両方に適しています。すべての塗り絵は無料でダウンロード、印刷、共有できます。さらに、デジタル塗り絵のためのオンラインゲームも提供しています。ウェブブラウザとモバイルアプリの両方をサポートしています。',
 }
 
 let homePage = {
@@ -170,11 +170,19 @@ let myWorks = {
 
 
 let play = {
-  zh: '开始游戏',
-  en: 'Play',
-  es: 'Jugar',
-  pt: 'Jogar',
-  ja: '遊ぶ',
+  zh: '在线填色',
+  en: 'Paint Online',
+  es: 'Pintar en línea',
+  pt: 'Pintar online',
+  ja: 'Paint Online',
+}
+
+let playOnApp = {
+  zh: '在APP上填色',
+  en: 'Paint On APP',
+  es: 'Aplicación Paint On',
+  pt: 'Aplicativo Paint On',
+  ja: 'Paint On アプリ',
 }
 
 let download = {
@@ -522,6 +530,21 @@ let refuse = {
   ja: '拒否する',
 }
 
+let interested = {
+  zh: '人对这副作品感兴趣',
+  en: 'people are interested in this work',
+  es: 'personas están interesadas en esta obra',
+  pt: 'pessoas estão interessadas nesta obra',
+  ja: '人がこの作品に興味を持っています',
+}
+
+let selectTag = {
+  zh: '选择一个标签来查找感兴趣的填色页',
+  en: 'Select a tag to find out the Coloring Pages',
+  es: 'Selecciona una etiqueta para encontrar las páginas de colorear.',
+  pt: 'Selecione uma tag para encontrar as páginas de colorir',
+  ja: 'タグを選択して、塗り絵のページを探します',
+}
 
 
 
@@ -548,6 +571,7 @@ let translate = {
   my,
   myWorks,
   play,
+  playOnApp,
   download,
   print,
   tag,
@@ -591,6 +615,8 @@ let translate = {
   learnMore,
   accept,
   refuse,
+  interested,
+  selectTag,
 }
 
 

+ 4 - 1
models/index.js

@@ -9,4 +9,7 @@ module.exports.Art = mongoose.model('Art', require('./schema-art'));
 module.exports.ArtAlbum = mongoose.model('Album', require('./schema-album'));
 module.exports.ArtDaily = mongoose.model('Daily', require('./schema-daily'));
 module.exports.ArtVideoStory = mongoose.model('VideoStory', require('./schema-video-story'));
-module.exports.Translate = mongoose.model('Translate', require('./schema-translate'));
+module.exports.Translate = mongoose.model('Translate', require('./schema-translate'));
+
+module.exports.TotalDoneRate = mongoose.model('Total_Done_Rate', require('./schema-total-done-rate'));
+module.exports.TagSeo = mongoose.model('TagSeo', require('./schema-tag-seo'));

+ 12 - 7
models/schema-art.js

@@ -56,14 +56,14 @@ let artSchema = new Schema({
   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 meta  description, 160个字符以内
-  desc: { type: String, desc: '作品描述', },  // json字符串,形如: {zh: '中国', en: 'China'}
-  // 新增字段,网站seo title,60个字符以内
+  // 标题
   title: { type: String, desc: '作品标题', }, // json字符串,形如: {zh: '中国', en: 'China'}
-  // 新增字段,小标题
-  subtitle: { type: String, desc: '小标题', searchable: true }, // json字符串,形如: {zh: '中国', en: 'China'}
-  // 文案描述, 200字左右
-  copy: { type: String, desc: '文案描述', searchable: true }, // json字符串,形如: {zh: '中国', en: 'China'}
+  // 文案描述,200字以内
+  desc: { type: String, desc: '作品描述', },  // json字符串,形如: {zh: '中国', en: 'China'}
+  // 网站seo title,60个字符以内
+  seoTitle: { type: String, desc: '小标题', searchable: true }, // json字符串,形如: {zh: '中国', en: 'China'}
+  // 网站seo meta  description, 160个字符以内
+  seoDescription: { type: String, desc: '文案描述', searchable: true }, // json字符串,形如: {zh: '中国', en: 'China'}
   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 },
 
@@ -109,6 +109,11 @@ let artSchema = new Schema({
 
   score: { type: Number, orderable: true, desc: '评分' }, // 总体评分0-5
 
+  ////////////////////////// 统计数据 ////////////////////////////
+  totalStartCount: { type: Number, index: true, orderable: true, desc: '总开始数' },
+  totalDoneCount: { type: Number, index: true, orderable: true, desc: '总完成数' },
+  completionRate: { type: Number, index: true, orderable: true, desc: '完成比例' },
+
   //数据库内不存储
   thumb: String,
   //zip: String,

+ 12 - 0
models/schema-tag-seo.js

@@ -0,0 +1,12 @@
+var Schema = require('mongoose').Schema;
+
+
+let tagSeo = new Schema({
+  tag: { type: String, unique: true, desc: 'tag' },
+  // 网站seo title,60个字符以内
+  seoTitle: { type: String, desc: '小标题', searchable: true }, // json字符串,形如: {zh: '中国', en: 'China'}
+  // 网站seo meta  description, 160个字符以内
+  seoDescription: { type: String, desc: '文案描述', searchable: true }, // json字符串,形如: {zh: '中国', en: 'China'}
+});
+
+module.exports = tagSeo;

+ 11 - 0
models/schema-total-done-rate.js

@@ -0,0 +1,11 @@
+// 完成率统计表
+var Schema = require('mongoose').Schema;
+
+
+let totalDoneRateSchema = new Schema({
+  totalStartCount: { type: Number, desc: '总开始数' },
+  totalDoneCount: { type: Number, desc: '总完成数' },
+  completionRate: { type: Number, desc: '完成比例' },
+});
+
+module.exports = totalDoneRateSchema;

+ 38 - 0
package-lock.json

@@ -24,6 +24,7 @@
         "mongoose": "^8.9.5",
         "node-cron": "^3.0.3",
         "node-fetch": "^2.7.0",
+        "pdf-lib": "^1.17.1",
         "sharp": "^0.33.5"
       }
     },
@@ -386,6 +387,22 @@
         "sparse-bitfield": "^3.0.3"
       }
     },
+    "node_modules/@pdf-lib/standard-fonts": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz",
+      "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==",
+      "dependencies": {
+        "pako": "^1.0.6"
+      }
+    },
+    "node_modules/@pdf-lib/upng": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz",
+      "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==",
+      "dependencies": {
+        "pako": "^1.0.10"
+      }
+    },
     "node_modules/@types/webidl-conversions": {
       "version": "7.0.3",
       "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
@@ -1430,6 +1447,11 @@
         "node": ">= 0.8"
       }
     },
+    "node_modules/pako": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+      "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
+    },
     "node_modules/parseurl": {
       "version": "1.3.3",
       "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -1443,6 +1465,22 @@
       "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
       "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
     },
+    "node_modules/pdf-lib": {
+      "version": "1.17.1",
+      "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz",
+      "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==",
+      "dependencies": {
+        "@pdf-lib/standard-fonts": "^1.0.0",
+        "@pdf-lib/upng": "^1.0.1",
+        "pako": "^1.0.11",
+        "tslib": "^1.11.1"
+      }
+    },
+    "node_modules/pdf-lib/node_modules/tslib": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+      "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
+    },
     "node_modules/proxy-addr": {
       "version": "2.0.7",
       "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",

+ 1 - 0
package.json

@@ -25,6 +25,7 @@
     "mongoose": "^8.9.5",
     "node-cron": "^3.0.3",
     "node-fetch": "^2.7.0",
+    "pdf-lib": "^1.17.1",
     "sharp": "^0.33.5"
   }
 }

+ 9 - 7
routes/index.js

@@ -4,7 +4,7 @@ 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 { tags, getTagSeoTitle, getTagSeoDescription } = require('../config/tag');
 const languages = require('../config/language');
 const translate = require('../config/translate');
 const meta = require('../config/meta');
@@ -16,9 +16,9 @@ const { getListBuilder } = require('../libs/pager');
 
 const CACHE_PREFIX = "art_v1";
 // const CACHE_EXPIRES = 60; // 60s刷新一次
-const CACHE_EXPIRES = 3600; // 1小时刷新一次即可
+const CACHE_EXPIRES = 600;
 
-const artSelect = 'name title desc seoTitle seoDescription width height date publishTime tags lastMod mystery hasSpecial useSpecialThumb publishVersion lock pageId';
+const artSelect = 'name title desc seoTitle seoDescription width height date publishTime tags lastMod mystery hasSpecial useSpecialThumb publishVersion totalStartCount totalDoneCount completionRate';
 
 
 // 路由:设置语言
@@ -377,9 +377,13 @@ router.get('/:lang/tag/:tag?', function (req, res, next) {
       let result = await getListBuilder(query, models.Art);
       organizeData(result.data, lang, imageType);
 
+      let title = tag == 'latest' ? meta.tagTitle[lang] : await getTagSeoTitle(tag, lang);
+      let description = await getTagSeoDescription(tag, lang);
+
       let data = {
-        title: `${tag} coloring pages`,
-        description: meta.tagDescription[lang],
+        title,
+        description,
+        h1title: tag == 'latest' ? translate.selectTag[lang] : `${tag} coloring pages`,
         data: result.data,
         page: result.page,
         length: result.length,
@@ -1444,7 +1448,6 @@ const organizeData = (data, lang, imageType) => {
     delete doc.hasSpecial;
     delete doc.useSpecialThumb;
     delete doc.publishVersion;
-    delete doc.pageId;
     delete doc.desc;
   })
 }
@@ -1507,7 +1510,6 @@ const organizeDetail = (doc, lang, imageType) => {
   delete doc.hasSpecial;
   delete doc.useSpecialThumb;
   delete doc.publishVersion;
-  delete doc.pageId;
 
 }
 

+ 81 - 0
routes/res/download.js

@@ -0,0 +1,81 @@
+var express = require('express');
+var router = express.Router();
+const utils = require('../../libs/utils');
+const config = require('../../config/app');
+const { PDFDocument, rgb, StandardFonts } = require('pdf-lib');
+
+
+router.get('/pdf/:id', function (req, res, next) {
+  (async function () {
+    let id = req.params.id;
+    utils.validators.validateId(id);
+
+    let host = config.cdnHost ?? config.resHost;
+
+    let url = `${host}/thumbs/coloring-page/page/1200/${id}.jpeg`;
+
+    try {
+      const response = await fetch(url);
+
+      if (!response.ok) {
+        throw new Error(`HTTP error! status: ${response.status}`);
+      }
+
+      const arrayBuffer = await response.arrayBuffer();
+
+      const padding = 40;
+      const pdfDoc = await PDFDocument.create();
+      const page = pdfDoc.addPage();
+
+      const image = await pdfDoc.embedJpg(arrayBuffer); // pdf-lib支持jpg,png
+
+      // 获取图片原始尺寸
+      const imageWidth = image.width;
+      const imageHeight = image.height;
+
+      // 计算带 padding 的图片绘制区域
+      const pageWidth = page.getWidth();
+      const pageHeight = page.getHeight();
+
+      // 计算缩放比例,保证图片在带 padding 的区域内
+      let scaledWidth = imageWidth;
+      let scaledHeight = imageHeight;
+
+      if (imageWidth + 2 * padding > pageWidth || imageHeight + 2 * padding > pageHeight) {
+        const widthScale = (pageWidth - 2 * padding) / imageWidth;
+        const heightScale = (pageHeight - 2 * padding) / imageHeight;
+        const scale = Math.min(widthScale, heightScale);
+        scaledWidth = imageWidth * scale;
+        scaledHeight = imageHeight * scale;
+      }
+
+      // 计算图片绘制的起始坐标
+      const x = (pageWidth - scaledWidth) / 2;
+      const y = (pageHeight - scaledHeight) / 2;
+
+      // 绘制图片,并留出 padding
+      page.drawImage(image, {
+        x: x,
+        y: y,
+        width: scaledWidth,
+        height: scaledHeight,
+      });
+
+
+      const pdfBytes = await pdfDoc.save();
+
+      res.setHeader('Content-Type', 'application/pdf');
+      res.setHeader('Content-Disposition', `attachment; filename=${id}.pdf`);
+      res.send(Buffer.from(pdfBytes));
+
+    } catch (error) {
+      console.error(`Error fetching image: ${id}`, error);
+    }
+
+
+  })().catch(next)
+});
+
+
+
+module.exports = router;

+ 1 - 1
service/cron-jobs/sitemap.js

@@ -6,7 +6,7 @@ const fs = require('fs');
 const datefns = require('date-fns');
 const models = require('../../models');
 const categories = require('../../config/category');
-const tags = require('../../config/tag');
+const { tags } = require('../config/tag');
 const date = datefns.format(Date.now(), 'yyyy-MM-dd');
 
 const sitemapPath = __dirname + '/../../dist/sitemap.xml';

+ 119 - 0
tools/init-tagseo.js

@@ -0,0 +1,119 @@
+const models = require('../models');
+const fetch = require('node-fetch');
+
+const { tags } = require('../config/tag');
+
+let apiKey = require('process').env.ARK_API_KEY;
+const url = "https://ark.cn-beijing.volces.com/api/v3/chat/completions";
+let headers = {
+  'Authorization': `Bearer ${apiKey}`,
+  'Content-Type': 'application/json'
+}
+
+/**
+ * 豆包对话API
+ * @param {*} text 
+ */
+async function fetchMetaByTxtFromDoubao(text) {
+  let data = {
+    "model": "ep-20250206115552-7qg5c", // Doubao-1.5...ion-pro-32k 当前最新,贵,响应慢,效果好
+    // "model": "ep-20250204231910-4phb8", // Doubao-vision-lite-32k  便宜点,相应速度快
+    "messages": [
+      { "role": "user", "content": text }
+    ]
+  }
+
+  console.log(data);
+
+  const jsonData = JSON.stringify(data);
+
+  const response = await fetch(url, { method: 'POST', headers, body: jsonData });
+
+  let responseJson = await response.json();
+
+  console.log(responseJson);
+
+  return responseJson.choices[0].message.content;
+}
+
+
+async function runTagMeta() {
+  let done = 0;
+  let duration = 0;
+  let hour, minute, second;
+  let start = Date.now();
+
+  let total = tags.length;
+  console.log('total:', total);
+  if (total <= 0) return;
+
+  for (let tag of tags) {
+    console.log(`process tag ${tag.tag}`);
+
+    console.time(tag.tag);
+
+    let doc = await models.TagSeo.findOne({ tag: tag.tag });
+    if (!doc) {
+      doc = new models.TagSeo({ tag: tag.tag });
+    } else {
+      console.log(`已经有${tag.tag}的seo信息,直接跳过`);
+      continue;
+    }
+
+    try {
+      let text = `生成 ${tag.tag} coloring pages 的SEO title(60个字符以内) 和 description(160个字符以内), 以json格式输出,支持语言中文(zh)、英语(en)、西班牙语(es)、葡萄牙语(pt)、日语(ja),形如: { title: {zh:'', en:'', es: '', pt: '', ja: ''}, description: {zh:'', en:'', es: '', pt: '', ja: ''}`;
+      let metaInfo = await fetchMetaByTxtFromDoubao(text);
+      console.log(metaInfo);
+      let metaInfoJson = JSON.parse(metaInfo);
+      let titleJson = metaInfoJson.title;
+      let descJson = metaInfoJson.description;
+      let title = JSON.stringify(titleJson);
+      let desc = JSON.stringify(descJson);
+
+
+      doc.seoTitle = title;
+      doc.seoDescription = desc;
+      await doc.save();
+
+    } catch (e) {
+      console.error(e.message);
+    }
+
+    console.timeEnd(tag.tag);
+
+    done++;
+    duration = (Date.now() - start) / 1000;
+    hour = (Math.floor(duration / 60 / 60)).toString().padStart(2, '0');
+    minute = (Math.floor(duration / 60) % 60).toString().padStart(2, '0');
+    second = (Math.floor(duration) % 60).toString().padStart(2, '0');
+
+    console.log('progress: ' + Math.floor((100 * done / total)) + '% used time: ' + hour + ':' + minute + ':' + second);
+
+  }
+}
+
+
+async function run() {
+  await runTagMeta();
+}
+
+
+async function test() {
+  let metaInfo = await fetchMetaByImageFromDoubao("https://color.jccytech.cn/thumbs/v2/work/640/67a254ec4f9d65537938e5c5.png");
+  console.log(metaInfo);
+  let metaInfoJson = JSON.parse(metaInfo);
+  let titleJson = metaInfoJson.title;
+  let descJson = metaInfoJson.copy;
+  let title = JSON.stringify(titleJson);
+  let desc = JSON.stringify(descJson);
+  console.log(title);
+  console.log(desc);
+}
+
+
+module.exports = { run }
+
+
+if (require.main == module) {
+  run();
+}

+ 33 - 0
tools/merge-statics.js

@@ -0,0 +1,33 @@
+// mongorestore --port 62701 -u coloring  -p coloring123. --authenticationDatabase=admin --db coloring_ol total_done_rates.bson  --drop
+
+const models = require('../models')
+
+
+
+// 初始化art表的areaCountFloor字段, 以及total_done_rate 统计相关的几个字段
+async function mergeStatics() {
+  console.time('mergeStatics');
+
+  let docs = await models.Art.find();
+  console.log(`total: ${docs.length}`);
+
+  for (let doc of docs) {
+    console.log(`process art: ${doc._id}`);
+    let totalDoneRateDoc = await models.TotalDoneRate.findById(doc._id);
+    if (totalDoneRateDoc) {
+      doc.totalStartCount = totalDoneRateDoc.totalStartCount;
+      doc.totalDoneCount = totalDoneRateDoc.totalDoneCount;
+      doc.completionRate = totalDoneRateDoc.completionRate;
+    }
+
+    await doc.save();
+  }
+
+  console.timeEnd('mergeStatics');
+}
+
+
+mergeStatics()
+  .then(console.log)
+  .catch(console.error)
+  .finally(() => require('process').exit());

+ 100 - 63
views/detail.ejs

@@ -8,6 +8,10 @@
         <link rel="stylesheet" href="/stylesheets/header.css">
         <link rel="stylesheet" href="/stylesheets/detail.css">
 
+        <script type="text/javascript"
+            src="https://platform-api.sharethis.com/js/sharethis.js#property=67e0d66a54a3d000192a4615&product=inline-share-buttons&source=platform"
+            async="async"></script>
+
         <script type="application/ld+json">
         {
             "@context": "https://schema.org",
@@ -61,37 +65,54 @@
         <div class="details">
             <div class="poster"><img src="<%= detail.thumb %>" alt="<%= detail.title %>"></div>
             <div class="description">
-                <h1>
-                    <%= detail.title %>
-                </h1>
-                <p>
-                    <%= translate.artist[lang] %>: <a href="/<%= lang %>/artist/<%= detail.user._id %>"
-                            class="tag-button">
-                            <%= detail.user.username %>
-                        </a>
-                </p>
-                <p>
-                    <%= translate.publishTime[lang] %>: <%= detail.publishTime %>
-                </p>
-                <p>
-                    <%= translate.tag[lang] %>:
-                        <% detail.tags.forEach(tag=> { %>
-                            <a href="/<%= lang %>/tag/<%= tag %>" class="tag-button">
-                                <%= tag %>
-                            </a>
-                            <% }); %>
-                </p>
-                <h2 style="font-size: 16px; font-weight:normal;">
-                    <%= detail.copy ?? detail.desc %>
-                </h2>
-                <div class="button-wrapper">
-                    <a href="/play/<%= detail._id %>" class="play-button">
-                        <%= translate.play[lang] %>
-                    </a>
-                    <a id="downloadBtn" class="play-button" style="background-color: lightseagreen;">
-                        <%= translate.download[lang] %>
-                    </a>
+                <div style="display: flex; justify-content: space-between;">
+                    <h1>
+                        <%= detail.title %>
+                    </h1>
+                    <div class="sharethis-inline-share-buttons"></div>
                 </div>
+                <% if (detail.totalStartCount> 0) { %>
+                    <div style="color:gray;">
+                        <%=detail.totalStartCount%>
+                            <%= translate.interested[lang] %>
+                    </div>
+                    <% } %>
+                        <p>
+                            <%= translate.artist[lang] %>: <a href="/<%= lang %>/artist/<%= detail.user._id %>"
+                                    class="tag-button">
+                                    <%= detail.user.username %>
+                                </a>
+                        </p>
+                        <p>
+                            <%= translate.publishTime[lang] %>: <%= detail.publishTime %>
+                        </p>
+                        <p>
+                            <%= translate.tag[lang] %>:
+                                <% detail.tags.forEach(tag=> { %>
+                                    <a href="/<%= lang %>/tag/<%= tag %>" class="tag-button">
+                                        <%= tag %>
+                                    </a>
+                                    <% }); %>
+                        </p>
+                        <p>
+                            <%= detail.copy ?? detail.desc %>
+                        </p>
+                        <div class="button-wrapper">
+                            <a href="/play/<%= detail._id %>" class="play-button">
+                                <%= translate.play[lang] %>
+                            </a>
+                            <a id="appBtn" class="play-button" style="background-color: darkolivegreen;">
+                                <%= translate.playOnApp[lang] %>
+                            </a>
+                            <a href="/download/pdf/<%= detail._id %>" class="play-button"
+                                style="background-color: lightseagreen;">
+                                <%= translate.download[lang] %>
+                            </a>
+                            <a id="printBtn" onclick="printImage('<%= detail._id %>')" class="play-button"
+                                style="background-color: black;">
+                                <%= translate.print[lang] %>
+                            </a>
+                        </div>
             </div>
         </div>
 
@@ -116,41 +137,57 @@
 
 
         <script>
-            // document.getElementById('downloadBtn').addEventListener('click', function () {
-            //     var link = document.createElement('a');
-            //     link.style.display = 'none';
-            //     link.href = '<%= detail.downlink %>';
-            //     link.download = '<%= detail._id %>.jpeg';
-            //     document.body.appendChild(link);
-            //     link.click();
-            //     document.body.removeChild(link);
-            // });
-
-            document.getElementById('downloadBtn').addEventListener('click', function () {
-                const imageUrl = '<%= detail.downlink %>';
-                fetch(imageUrl)
-                    .then(response => {
-                        if (!response.ok) {
-                            throw new Error('Network response was not ok');
-                        }
-                        return response.blob();
-                    })
-                    .then(blob => {
-                        const url = URL.createObjectURL(blob);
-                        const a = document.createElement('a');
-                        a.style.display = 'none';
-                        a.href = url;
-                        a.download = '<%= detail._id %>.webp';
-                        document.body.appendChild(a);
-                        a.click();
-                        URL.revokeObjectURL(url);
-                        document.body.removeChild(a);
-                    })
-                    .catch(error => {
-                        console.error('Image Download Error:', error);
-                    });
+            function isMobileDevice() {
+                return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
+            }
+
+            window.onload = function () {
+                if (isMobileDevice()) {
+                    const printBtn = document.getElementById('printBtn');
+                    printBtn.style.display = 'none'; // 移动端隐藏打印按钮
+                }
+            };
+
+            document.getElementById('appBtn').addEventListener('click', function () {
+                const userAgent = navigator.userAgent || navigator.vendor || window.opera;
+
+                // Android 检测
+                if (/android/i.test(userAgent)) {
+                    window.location.href = 'https://play.google.com/store/apps/details?id=com.pcoloring.art.puzzle.color.by.number';
+                }
+                // iOS 检测
+                else if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) {
+                    window.location.href = 'https://apps.apple.com/gb/app/art-number-coloring-book/id1575480118';
+                }
+                // 其他操作系统(例如桌面)
+                else {
+                    // 可以显示一个提示,或者跳转到通用的下载页面
+                    console.log('无法确定操作系统,或者为桌面操作系统');
+                    window.location.href = 'https://pcoloring.com/anc/';
+                }
             });
 
+            async function printImage(id) {
+                try {
+                    const response = await fetch(`/download/pdf/${id}`);
+
+                    if (!response.ok) {
+                        throw new Error(`HTTP error! status: ${response.status}`);
+                    }
+
+                    const pdfBlob = await response.blob();
+                    const pdfUrl = URL.createObjectURL(pdfBlob);
+
+                    const printWindow = window.open(pdfUrl, '_blank');
+                    printWindow.onload = () => {
+                        printWindow.print();
+                    };
+
+                    URL.revokeObjectURL(pdfUrl); // 释放 URL 对象
+                } catch (error) {
+                    console.error('Error printing image:', error);
+                }
+            }
         </script>
 </body>
 

+ 23 - 22
views/tag.ejs

@@ -61,33 +61,34 @@
   <%- include('header') %>
 
     <h1 style="display: flex; justify-content: center; padding: 10px; color: purple">
-      <%= title %>
+      <%= h1title %>
     </h1>
-    <!-- <h2 style="display: flex; justify-content: center; padding: 0px 10px 10px 10px; color: #333">
-      <%= description %>
-    </h2> -->
 
-    <div class="content">
-      <div class="image-grid">
-        <% data.forEach(item=> { %>
-          <div class="image-card">
-            <a href="<%= item.uri %>"><img src="<%= item.thumb %>" alt="<%= item.title %>"></a>
-            <div class="card-title"><%= item.title %></div>
-          </div>
-          <% }); %>
+    <% if (tag !='latest' ) { %>
+      <div class="content">
+        <div class="image-grid">
+          <% data.forEach(item=> { %>
+            <div class="image-card">
+              <a href="<%= item.uri %>"><img src="<%= item.thumb %>" alt="<%= item.title %>"></a>
+              <div class="card-title">
+                <%= item.title %>
+              </div>
+            </div>
+            <% }); %>
+        </div>
       </div>
-    </div>
 
-    <%- include('pagination') %>
+      <%- include('pagination') %>
+        <% } %>
 
-      <div class="tag-cloud">
-        <% tags.forEach(item=> { %>
-          <a href="/<%= lang %>/tag/<%= item.tag %>" class="tag <%= item.tag == tag ? 'selected' : '' %>"
-            style="color: <%= item.color %>;">
-            <%= item.tag %>
-          </a>
-          <% }); %>
-      </div>
+          <div class="tag-cloud">
+            <% tags.forEach(item=> { %>
+              <a href="/<%= lang %>/tag/<%= item.tag %>" class="tag <%= item.tag == tag ? 'selected' : '' %>"
+                style="color: <%= item.color %>;">
+                <%= item.tag %>
+              </a>
+              <% }); %>
+          </div>
 
 </body>