liyuliangjiazai 3 kuukautta sitten
vanhempi
sitoutus
561f2a68c3

+ 11 - 2
code/ajyApp/manifest.json

@@ -19,7 +19,8 @@
         /* 模块配置 */
         "modules" : {
             "Bluetooth" : {},
-            "Push" : {}
+            "Push" : {},
+            "Camera" : {}
         },
         /* 应用发布信息 */
         "distribute" : {
@@ -45,7 +46,15 @@
             },
             /* ios打包配置 */
             "ios" : {
-                "dSYMs" : false
+                "dSYMs" : false,
+                "privacyDescription" : {
+                    "NSPhotoLibraryUsageDescription" : "需要访问您的相册,以便您选择照片作为头像或发布内容",
+                    "NSPhotoLibraryAddUsageDescription" : "需要保存图片/视频到相册,方便您随时查看",
+                    "NSCameraUsageDescription" : "需要访问摄像头,以便您拍摄照片、录制视频或进行扫码",
+                    "NSMicrophoneUsageDescription" : "录制视频时需要使用麦克风采集声音",
+                    "NSBluetoothAlwaysUsageDescription" : "需要访问蓝牙,以便搜索并连接附近的智能设备",
+                    "NSBluetoothPeripheralUsageDescription" : "需要访问蓝牙,以便与周边设备进行数据交互"
+                }
             },
             /* SDK配置 */
             "sdkConfigs" : {

+ 44 - 15
code/backend/src/main/java/com/aijiuyi/admin/controller/ArticleViewController.java

@@ -55,7 +55,10 @@ public class ArticleViewController {
      */
     private String buildArticlePage(ContentArticle article) {
         String watermarkText = article.getWatermarkText();
-        boolean hasWatermark = watermarkText != null && !watermarkText.trim().isEmpty();
+        String watermarkImage = article.getWatermarkImage();
+        boolean hasTextWatermark = watermarkText != null && !watermarkText.trim().isEmpty();
+        boolean hasImageWatermark = watermarkImage != null && !watermarkImage.trim().isEmpty();
+        boolean hasWatermark = hasTextWatermark || hasImageWatermark;
         String title = escapeHtml(article.getTitle() != null ? article.getTitle() : "");
         String author = escapeHtml(article.getAuthor() != null ? article.getAuthor() : "");
         String publishTime = article.getPublishTime() != null ? article.getPublishTime().toString().replace("T", " ") : "";
@@ -89,7 +92,12 @@ public class ArticleViewController {
         // 水印样式
         if (hasWatermark) {
             html.append(".watermark { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 9999; overflow: hidden; }\n");
-            html.append(".watermark-text { position: absolute; color: rgba(0,0,0,0.06); font-size: 16px; transform: rotate(-30deg); white-space: nowrap; font-weight: 500; }\n");
+            if (hasImageWatermark) {
+                html.append(".watermark-item { position: absolute; transform: rotate(-30deg); opacity: 0.08; }\n");
+                html.append(".watermark-item img { width: 120px; height: auto; }\n");
+            } else {
+                html.append(".watermark-text { position: absolute; color: rgba(0,0,0,0.06); font-size: 16px; transform: rotate(-30deg); white-space: nowrap; font-weight: 500; }\n");
+            }
         }
         // 移动端适配
         html.append("@media (max-width: 768px) {\n");
@@ -150,19 +158,40 @@ public class ArticleViewController {
             html.append("\n// 生成水印\n");
             html.append("(function() {\n");
             html.append("  var container = document.getElementById('watermark');\n");
-            html.append("  var text = '").append(escapeJs(watermarkText.trim())).append("';\n");
-            html.append("  var cols = Math.ceil(window.innerWidth / 200);\n");
-            html.append("  var rows = Math.ceil(window.innerHeight / 150);\n");
-            html.append("  for (var i = 0; i < rows + 5; i++) {\n");
-            html.append("    for (var j = 0; j < cols + 3; j++) {\n");
-            html.append("      var span = document.createElement('span');\n");
-            html.append("      span.className = 'watermark-text';\n");
-            html.append("      span.textContent = text;\n");
-            html.append("      span.style.left = (j * 200 - 50) + 'px';\n");
-            html.append("      span.style.top = (i * 150 - 30) + 'px';\n");
-            html.append("      container.appendChild(span);\n");
-            html.append("    }\n");
-            html.append("  }\n");
+            if (hasImageWatermark) {
+                // 图片水印优先
+                html.append("  var imgUrl = '").append(escapeJs(watermarkImage.trim())).append("';\n");
+                html.append("  var cols = Math.ceil(window.innerWidth / 240);\n");
+                html.append("  var rows = Math.ceil(window.innerHeight / 200);\n");
+                html.append("  for (var i = 0; i < rows + 5; i++) {\n");
+                html.append("    for (var j = 0; j < cols + 3; j++) {\n");
+                html.append("      var div = document.createElement('div');\n");
+                html.append("      div.className = 'watermark-item';\n");
+                html.append("      var img = document.createElement('img');\n");
+                html.append("      img.src = imgUrl;\n");
+                html.append("      img.draggable = false;\n");
+                html.append("      div.appendChild(img);\n");
+                html.append("      div.style.left = (j * 240 - 60) + 'px';\n");
+                html.append("      div.style.top = (i * 200 - 40) + 'px';\n");
+                html.append("      container.appendChild(div);\n");
+                html.append("    }\n");
+                html.append("  }\n");
+            } else {
+                // 文本水印
+                html.append("  var text = '").append(escapeJs(watermarkText.trim())).append("';\n");
+                html.append("  var cols = Math.ceil(window.innerWidth / 200);\n");
+                html.append("  var rows = Math.ceil(window.innerHeight / 150);\n");
+                html.append("  for (var i = 0; i < rows + 5; i++) {\n");
+                html.append("    for (var j = 0; j < cols + 3; j++) {\n");
+                html.append("      var span = document.createElement('span');\n");
+                html.append("      span.className = 'watermark-text';\n");
+                html.append("      span.textContent = text;\n");
+                html.append("      span.style.left = (j * 200 - 50) + 'px';\n");
+                html.append("      span.style.top = (i * 150 - 30) + 'px';\n");
+                html.append("      container.appendChild(span);\n");
+                html.append("    }\n");
+                html.append("  }\n");
+            }
             html.append("})();\n");
         }
 

+ 3 - 0
code/backend/src/main/java/com/aijiuyi/admin/entity/ContentArticle.java

@@ -51,6 +51,9 @@ public class ContentArticle {
     /** 水印文本(为NULL或空则不显示水印,有内容则显示对应水印) */
     private String watermarkText;
 
+    /** 水印图片URL(上传到OSS的图片地址,优先级高于文本水印) */
+    private String watermarkImage;
+
     /** 发布时间 */
     @TableField(updateStrategy = FieldStrategy.IGNORED)
     private LocalDateTime publishTime;

+ 6 - 0
code/backend/src/main/resources/sql/migration_article_watermark_image.sql

@@ -0,0 +1,6 @@
+-- ==================== 内容文章表:添加水印图片字段 ====================
+-- 执行数据库:aijiuyi_admin
+-- 作用:为文章添加水印图片URL字段,上传图片到OSS后存储URL,图片水印优先级高于文本水印
+
+ALTER TABLE `content_article`
+    ADD COLUMN `watermark_image` VARCHAR(500) DEFAULT NULL COMMENT '水印图片URL(OSS地址,优先级高于文本水印)' AFTER `watermark_text`;

+ 3 - 2
code/frontend/src/components/RichEditor.vue

@@ -24,6 +24,8 @@ import 'tinymce/tinymce'
 import 'tinymce/themes/silver'
 import 'tinymce/icons/default'
 import 'tinymce/models/dom'
+// 直接导入中文语言包(打包进 bundle,避免运行时加载路径问题)
+import '@/utils/tinymce-zh_CN'
 // 导入所需插件
 import 'tinymce/plugins/image'
 import 'tinymce/plugins/media'
@@ -91,9 +93,8 @@ const editorConfig = {
   skin_url: '/tinymce/skins/ui/oxide',
   content_css: '/tinymce/skins/content/default/content.min.css',
 
-  // 中文语言配置
+  // 中文语言配置(语言包已通过 import 注册,无需 language_url)
   language: 'zh_CN',
-  language_url: '/tinymce/langs/zh_CN.js',
 
   // 编辑器基本设置
   height: editorHeight.value,

+ 367 - 0
code/frontend/src/utils/tinymce-zh_CN.js

@@ -0,0 +1,367 @@
+/**
+ * TinyMCE 中文语言包(直接打包进 bundle,避免运行时加载路径问题)
+ */
+import tinymce from 'tinymce/tinymce'
+
+tinymce.addI18n('zh_CN', {
+  "Redo": "\u91cd\u505a",
+  "Undo": "\u64a4\u9500",
+  "Cut": "\u526a\u5207",
+  "Copy": "\u590d\u5236",
+  "Paste": "\u7c98\u8d34",
+  "Select all": "\u5168\u9009",
+  "New document": "\u65b0\u6587\u6863",
+  "Ok": "\u786e\u5b9a",
+  "Cancel": "\u53d6\u6d88",
+  "Visual aids": "\u7f51\u683c\u7ebf",
+  "Bold": "\u52a0\u7c97",
+  "Italic": "\u659c\u4f53",
+  "Underline": "\u4e0b\u5212\u7ebf",
+  "Strikethrough": "\u5220\u9664\u7ebf",
+  "Superscript": "\u4e0a\u6807",
+  "Subscript": "\u4e0b\u6807",
+  "Clear formatting": "\u6e05\u9664\u683c\u5f0f",
+  "Remove": "\u5220\u9664",
+  "Align left": "\u5de6\u5bf9\u9f50",
+  "Align center": "\u5c45\u4e2d\u5bf9\u9f50",
+  "Align right": "\u53f3\u5bf9\u9f50",
+  "No alignment": "\u65e0\u5bf9\u9f50",
+  "Justify": "\u4e24\u7aef\u5bf9\u9f50",
+  "Bullet list": "\u65e0\u5e8f\u5217\u8868",
+  "Numbered list": "\u6709\u5e8f\u5217\u8868",
+  "Decrease indent": "\u51cf\u5c11\u7f29\u8fdb",
+  "Increase indent": "\u589e\u52a0\u7f29\u8fdb",
+  "Close": "\u5173\u95ed",
+  "Formats": "\u683c\u5f0f",
+  "Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X/C/V keyboard shortcuts instead.": "\u4f60\u7684\u6d4f\u89c8\u5668\u4e0d\u652f\u6301\u5bf9\u526a\u8d34\u677f\u7684\u8bbf\u95ee\uff0c\u8bf7\u4f7f\u7528Ctrl+X/C/V\u952e\u8fdb\u884c\u590d\u5236\u7c98\u8d34\u3002",
+  "Headings": "\u6807\u9898",
+  "Heading 1": "\u6807\u98981",
+  "Heading 2": "\u6807\u98982",
+  "Heading 3": "\u6807\u98983",
+  "Heading 4": "\u6807\u98984",
+  "Heading 5": "\u6807\u98985",
+  "Heading 6": "\u6807\u98986",
+  "Preformatted": "\u9884\u683c\u5f0f\u5316",
+  "Div": "Div",
+  "Pre": "\u9884\u683c\u5f0f",
+  "Code": "\u4ee3\u7801",
+  "Paragraph": "\u6bb5\u843d",
+  "Blockquote": "\u5f15\u7528",
+  "Inline": "\u884c\u5185",
+  "Blocks": "\u533a\u5757",
+  "Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.": "\u5f53\u524d\u4e3a\u7eaf\u6587\u672c\u7c98\u8d34\u6a21\u5f0f\uff0c\u518d\u6b21\u70b9\u51fb\u53ef\u4ee5\u56de\u5230\u666e\u901a\u7c98\u8d34\u6a21\u5f0f\u3002",
+  "Fonts": "\u5b57\u4f53",
+  "Font sizes": "\u5b57\u53f7",
+  "Class": "\u7c7b\u578b",
+  "Browse for an image": "\u6d4f\u89c8\u56fe\u7247",
+  "OR": "\u6216",
+  "Drop an image here": "\u62d6\u653e\u4e00\u5f20\u56fe\u7247\u81f3\u6b64",
+  "Upload": "\u4e0a\u4f20",
+  "Uploading image": "\u4e0a\u4f20\u56fe\u7247\u4e2d",
+  "Block": "\u5757",
+  "Align": "\u5bf9\u9f50",
+  "Default": "\u9ed8\u8ba4",
+  "Circle": "\u7a7a\u5fc3\u5706",
+  "Disc": "\u5b9e\u5fc3\u5706",
+  "Square": "\u65b9\u5757",
+  "Lower Alpha": "\u5c0f\u5199\u82f1\u6587\u5b57\u6bcd",
+  "Lower Greek": "\u5c0f\u5199\u5e0c\u814a\u5b57\u6bcd",
+  "Lower Roman": "\u5c0f\u5199\u7f57\u9a6c\u5b57\u6bcd",
+  "Upper Alpha": "\u5927\u5199\u82f1\u6587\u5b57\u6bcd",
+  "Upper Roman": "\u5927\u5199\u7f57\u9a6c\u5b57\u6bcd",
+  "Anchor...": "\u951a\u70b9...",
+  "Anchor": "\u951a\u70b9",
+  "Name": "\u540d\u79f0",
+  "ID": "ID",
+  "ID should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.": "ID\u5e94\u8be5\u4ee5\u5b57\u6bcd\u5f00\u5934\uff0c\u540e\u8ddf\u5b57\u6bcd\u3001\u6570\u5b57\u3001\u7834\u6298\u53f7\u3001\u70b9\u3001\u5192\u53f7\u6216\u4e0b\u5212\u7ebf\u3002",
+  "You have unsaved changes are you sure you want to navigate away?": "\u4f60\u8fd8\u6709\u6587\u6863\u5c1a\u672a\u4fdd\u5b58\uff0c\u786e\u5b9a\u8981\u79bb\u5f00\uff1f",
+  "Restore last draft": "\u6062\u590d\u4e0a\u6b21\u7684\u8349\u7a3f",
+  "Special character...": "\u7279\u6b8a\u5b57\u7b26...",
+  "Special Character": "\u7279\u6b8a\u5b57\u7b26",
+  "Source code": "\u6e90\u4ee3\u7801",
+  "Insert/Edit code sample": "\u63d2\u5165/\u7f16\u8f91\u4ee3\u7801\u793a\u4f8b",
+  "Language": "\u8bed\u8a00",
+  "Code sample...": "\u4ee3\u7801\u793a\u4f8b...",
+  "Left to right": "\u4ece\u5de6\u5230\u53f3",
+  "Right to left": "\u4ece\u53f3\u5230\u5de6",
+  "Title": "\u6807\u9898",
+  "Fullscreen": "\u5168\u5c4f",
+  "Action": "\u64cd\u4f5c",
+  "Shortcut": "\u5feb\u6377\u952e",
+  "Help": "\u5e2e\u52a9",
+  "Address": "\u5730\u5740",
+  "Focus to menubar": "\u79fb\u52a8\u7126\u70b9\u5230\u83dc\u5355\u680f",
+  "Focus to toolbar": "\u79fb\u52a8\u7126\u70b9\u5230\u5de5\u5177\u680f",
+  "Focus to element path": "\u79fb\u52a8\u7126\u70b9\u5230\u5143\u7d20\u8def\u5f84",
+  "Focus to contextual toolbar": "\u79fb\u52a8\u7126\u70b9\u5230\u4e0a\u4e0b\u6587\u83dc\u5355",
+  "Insert link (if link plugin activated)": "\u63d2\u5165\u94fe\u63a5",
+  "Save (if save plugin activated)": "\u4fdd\u5b58",
+  "Find (if searchreplace plugin activated)": "\u67e5\u627e",
+  "Plugins installed ({0}):": "\u5df2\u5b89\u88c5\u63d2\u4ef6 ({0}):",
+  "Premium plugins:": "\u4ed8\u8d39\u63d2\u4ef6\uff1a",
+  "Learn more...": "\u4e86\u89e3\u66f4\u591a...",
+  "You are using {0}": "\u4f60\u6b63\u5728\u4f7f\u7528 {0}",
+  "Plugins": "\u63d2\u4ef6",
+  "Handy Shortcuts": "\u5feb\u6377\u952e",
+  "Horizontal line": "\u6c34\u5e73\u5206\u5272\u7ebf",
+  "Insert/edit image": "\u63d2\u5165/\u7f16\u8f91\u56fe\u7247",
+  "Alternative description": "\u66ff\u4ee3\u63cf\u8ff0",
+  "Accessibility": "\u8f85\u52a9\u529f\u80fd",
+  "Image is decorative": "\u56fe\u7247\u662f\u88c5\u9970\u6027\u7684",
+  "Source": "\u5730\u5740",
+  "Dimensions": "\u5927\u5c0f",
+  "Constrain proportions": "\u4fdd\u6301\u7eb5\u6a2a\u6bd4",
+  "General": "\u666e\u901a",
+  "Advanced": "\u9ad8\u7ea7",
+  "Style": "\u6837\u5f0f",
+  "Vertical space": "\u5782\u76f4\u8fb9\u8ddd",
+  "Horizontal space": "\u6c34\u5e73\u8fb9\u8ddd",
+  "Border": "\u8fb9\u6846",
+  "Insert image": "\u63d2\u5165\u56fe\u7247",
+  "Image...": "\u56fe\u7247...",
+  "Image list": "\u56fe\u7247\u5217\u8868",
+  "Resize": "\u8c03\u6574\u5927\u5c0f",
+  "Insert date/time": "\u63d2\u5165\u65e5\u671f/\u65f6\u95f4",
+  "Date/time": "\u65e5\u671f/\u65f6\u95f4",
+  "Insert/edit link": "\u63d2\u5165/\u7f16\u8f91\u94fe\u63a5",
+  "Text to display": "\u663e\u793a\u6587\u5b57",
+  "Url": "\u5730\u5740",
+  "Open link in...": "\u94fe\u63a5\u6253\u5f00\u65b9\u5f0f...",
+  "Current window": "\u5f53\u524d\u7a97\u53e3",
+  "None": "\u65e0",
+  "New window": "\u65b0\u7a97\u53e3",
+  "Open link": "\u6253\u5f00\u94fe\u63a5",
+  "Remove link": "\u5220\u9664\u94fe\u63a5",
+  "Anchors": "\u951a\u70b9",
+  "Link...": "\u94fe\u63a5...",
+  "Paste or type a link": "\u7c98\u8d34\u6216\u8f93\u5165\u94fe\u63a5",
+  "The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "\u4f60\u6240\u586b\u5199\u7684URL\u5730\u5740\u4e3a\u90ae\u4ef6\u5730\u5740\uff0c\u9700\u8981\u52a0\u4e0amailto:\u524d\u7f00\u5417\uff1f",
+  "The URL you entered seems to be an external link. Do you want to add the required http:// prefix?": "\u4f60\u6240\u586b\u5199\u7684URL\u5730\u5740\u5c5e\u4e8e\u5916\u90e8\u94fe\u63a5\uff0c\u9700\u8981\u52a0\u4e0ahttp://\u524d\u7f00\u5417\uff1f",
+  "The URL you entered seems to be an external link. Do you want to add the required https:// prefix?": "\u4f60\u6240\u586b\u5199\u7684URL\u5730\u5740\u5c5e\u4e8e\u5916\u90e8\u94fe\u63a5\uff0c\u9700\u8981\u52a0\u4e0ahttps://\u524d\u7f00\u5417\uff1f",
+  "Link list": "\u94fe\u63a5\u5217\u8868",
+  "Insert video": "\u63d2\u5165\u89c6\u9891",
+  "Insert/edit video": "\u63d2\u5165/\u7f16\u8f91\u89c6\u9891",
+  "Insert/edit media": "\u63d2\u5165/\u7f16\u8f91\u5a92\u4f53",
+  "Alternative source": "\u955c\u50cf",
+  "Alternative source URL": "\u66ff\u4ee3\u6765\u6e90\u7f51\u5740",
+  "Media poster (Image URL)": "\u5c01\u9762\uff08\u56fe\u7247\u5730\u5740\uff09",
+  "Paste your embed code below:": "\u5c06\u5185\u5d4c\u4ee3\u7801\u7c98\u8d34\u5728\u4e0b\u9762:",
+  "Embed": "\u5185\u5d4c",
+  "Media...": "\u591a\u5a92\u4f53...",
+  "Nonbreaking space": "\u4e0d\u95f4\u65ad\u7a7a\u683c",
+  "Page break": "\u5206\u9875\u7b26",
+  "Paste as text": "\u7c98\u8d34\u4e3a\u6587\u672c",
+  "Preview": "\u9884\u89c8",
+  "Print": "\u6253\u5370",
+  "Print...": "\u6253\u5370...",
+  "Save": "\u4fdd\u5b58",
+  "Find": "\u67e5\u627e",
+  "Replace with": "\u66ff\u6362\u4e3a",
+  "Replace": "\u66ff\u6362",
+  "Replace all": "\u5168\u90e8\u66ff\u6362",
+  "Previous": "\u4e0a\u4e00\u4e2a",
+  "Next": "\u4e0b\u4e00\u4e2a",
+  "Find and Replace": "\u67e5\u627e\u548c\u66ff\u6362",
+  "Find and replace...": "\u67e5\u627e\u548c\u66ff\u6362...",
+  "Could not find the specified string.": "\u672a\u627e\u5230\u641c\u7d22\u5185\u5bb9.",
+  "Match case": "\u533a\u5206\u5927\u5c0f\u5199",
+  "Find whole words only": "\u5168\u5b57\u5339\u914d",
+  "Find in selection": "\u5728\u9009\u533a\u4e2d\u67e5\u627e",
+  "Insert table": "\u63d2\u5165\u8868\u683c",
+  "Table properties": "\u8868\u683c\u5c5e\u6027",
+  "Delete table": "\u5220\u9664\u8868\u683c",
+  "Cell": "\u5355\u5143\u683c",
+  "Row": "\u884c",
+  "Column": "\u5217",
+  "Cell properties": "\u5355\u5143\u683c\u5c5e\u6027",
+  "Merge cells": "\u5408\u5e76\u5355\u5143\u683c",
+  "Split cell": "\u62c6\u5206\u5355\u5143\u683c",
+  "Insert row before": "\u5728\u4e0a\u65b9\u63d2\u5165",
+  "Insert row after": "\u5728\u4e0b\u65b9\u63d2\u5165",
+  "Delete row": "\u5220\u9664\u884c",
+  "Row properties": "\u884c\u5c5e\u6027",
+  "Cut row": "\u526a\u5207\u884c",
+  "Cut column": "\u526a\u5207\u5217",
+  "Copy row": "\u590d\u5236\u884c",
+  "Copy column": "\u590d\u5236\u5217",
+  "Paste row before": "\u7c98\u8d34\u5230\u4e0a\u65b9",
+  "Paste row after": "\u7c98\u8d34\u5230\u4e0b\u65b9",
+  "Paste column before": "\u7c98\u8d34\u5230\u524d\u65b9",
+  "Paste column after": "\u7c98\u8d34\u5230\u540e\u65b9",
+  "Insert column before": "\u5728\u5de6\u4fa7\u63d2\u5165",
+  "Insert column after": "\u5728\u53f3\u4fa7\u63d2\u5165",
+  "Delete column": "\u5220\u9664\u5217",
+  "Cols": "\u5217",
+  "Rows": "\u884c",
+  "Width": "\u5bbd",
+  "Height": "\u9ad8",
+  "Cell spacing": "\u5355\u5143\u683c\u5916\u95f4\u8ddd",
+  "Cell padding": "\u5355\u5143\u683c\u5185\u8fb9\u8ddd",
+  "Row clipboard actions": "\u884c\u526a\u8d34\u677f\u64cd\u4f5c",
+  "Column clipboard actions": "\u5217\u526a\u8d34\u677f\u64cd\u4f5c",
+  "Table styles": "\u8868\u683c\u6837\u5f0f",
+  "Cell styles": "\u5355\u5143\u683c\u6837\u5f0f",
+  "Column header": "\u5217\u5934",
+  "Row header": "\u884c\u5934",
+  "Table caption": "\u8868\u683c\u6807\u9898",
+  "Caption": "\u6807\u9898",
+  "Show caption": "\u663e\u793a\u6807\u9898",
+  "Left": "\u5de6\u5bf9\u9f50",
+  "Center": "\u5c45\u4e2d",
+  "Right": "\u53f3\u5bf9\u9f50",
+  "Cell type": "\u5355\u5143\u683c\u7c7b\u578b",
+  "Scope": "\u8303\u56f4",
+  "Alignment": "\u5bf9\u9f50\u65b9\u5f0f",
+  "Horizontal align": "\u6c34\u5e73\u5bf9\u9f50",
+  "Vertical align": "\u5782\u76f4\u5bf9\u9f50",
+  "Top": "\u9876\u90e8\u5bf9\u9f50",
+  "Middle": "\u5782\u76f4\u5c45\u4e2d",
+  "Bottom": "\u5e95\u90e8\u5bf9\u9f50",
+  "Header cell": "\u8868\u5934\u5355\u5143\u683c",
+  "Row group": "\u884c\u7ec4",
+  "Column group": "\u5217\u7ec4",
+  "Row type": "\u884c\u7c7b\u578b",
+  "Header": "\u8868\u5934",
+  "Body": "\u8868\u4f53",
+  "Footer": "\u8868\u5c3e",
+  "Border color": "\u8fb9\u6846\u989c\u8272",
+  "Solid": "\u5b9e\u7ebf",
+  "Dotted": "\u70b9\u7ebf",
+  "Dashed": "\u865a\u7ebf",
+  "Double": "\u53cc\u7ebf",
+  "Groove": "\u51f9\u69fd",
+  "Ridge": "\u5782\u810a",
+  "Inset": "\u5185\u51f9",
+  "Outset": "\u5916\u51f8",
+  "Hidden": "\u9690\u85cf",
+  "Insert template...": "\u63d2\u5165\u6a21\u677f...",
+  "Templates": "\u6a21\u677f",
+  "Template": "\u6a21\u677f",
+  "Insert Template": "\u63d2\u5165\u6a21\u677f",
+  "Text color": "\u6587\u5b57\u989c\u8272",
+  "Background color": "\u80cc\u666f\u8272",
+  "Custom...": "\u81ea\u5b9a\u4e49...",
+  "Custom color": "\u81ea\u5b9a\u4e49\u989c\u8272",
+  "No color": "\u65e0",
+  "Remove color": "\u79fb\u9664\u989c\u8272",
+  "Show blocks": "\u663e\u793a\u533a\u5757\u8fb9\u6846",
+  "Show invisible characters": "\u663e\u793a\u4e0d\u53ef\u89c1\u5b57\u7b26",
+  "Word count": "\u5b57\u6570\u7edf\u8ba1",
+  "Count": "\u8ba1\u6570",
+  "Document": "\u6587\u6863",
+  "Selection": "\u9009\u62e9",
+  "Words": "\u5355\u8bcd",
+  "Words: {0}": "\u5b57\u6570\uff1a{0}",
+  "{0} words": "{0} \u4e2a\u5b57",
+  "File": "\u6587\u4ef6",
+  "Edit": "\u7f16\u8f91",
+  "Insert": "\u63d2\u5165",
+  "View": "\u89c6\u56fe",
+  "Format": "\u683c\u5f0f",
+  "Table": "\u8868\u683c",
+  "Tools": "\u5de5\u5177",
+  "Powered by {0}": "\u7531 {0} \u9a71\u52a8",
+  "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "\u5728\u7f16\u8f91\u533a\u6309ALT-F9\u6253\u5f00\u83dc\u5355\uff0c\u6309ALT-F10\u6253\u5f00\u5de5\u5177\u680f\uff0c\u6309ALT-0\u67e5\u770b\u5e2e\u52a9",
+  "Image title": "\u56fe\u7247\u6807\u9898",
+  "Border width": "\u8fb9\u6846\u5bbd\u5ea6",
+  "Border style": "\u8fb9\u6846\u6837\u5f0f",
+  "Error": "\u9519\u8bef",
+  "Warn": "\u8b66\u544a",
+  "Valid": "\u6709\u6548",
+  "To open the popup, press Shift+Enter": "\u6309Shift+Enter\u6253\u5f00\u5bf9\u8bdd\u6846",
+  "Rich Text Area": "\u5bcc\u6587\u672c\u7f16\u8f91\u533a",
+  "Rich Text Area. Press ALT-0 for help.": "\u5bcc\u6587\u672c\u7f16\u8f91\u533a\u3002\u6309ALT-0\u83b7\u53d6\u5e2e\u52a9\u3002",
+  "System Font": "\u7cfb\u7edf\u5b57\u4f53",
+  "Failed to upload image: {0}": "\u56fe\u7247\u4e0a\u4f20\u5931\u8d25: {0}",
+  "Failed to load plugin: {0} from url {1}": "\u63d2\u4ef6\u52a0\u8f7d\u5931\u8d25: {0} \u4ece {1}",
+  "Failed to load plugin url: {0}": "\u63d2\u4ef6\u52a0\u8f7d\u5931\u8d25: {0}",
+  "Failed to initialize plugin: {0}": "\u63d2\u4ef6\u521d\u59cb\u5316\u5931\u8d25: {0}",
+  "example": "\u793a\u4f8b",
+  "Search": "\u641c\u7d22",
+  "All": "\u5168\u90e8",
+  "Currency": "\u8d27\u5e01",
+  "Text": "\u6587\u672c",
+  "Quotations": "\u5f15\u53f7",
+  "Mathematical": "\u6570\u5b66",
+  "Extended Latin": "\u62c9\u4e01\u8bed\u6269\u5c55",
+  "Symbols": "\u7b26\u53f7",
+  "Arrows": "\u7bad\u5934",
+  "User Defined": "\u81ea\u5b9a\u4e49",
+  "dollar sign": "\u7f8e\u5143\u7b26\u53f7",
+  "currency sign": "\u8d27\u5e01\u7b26\u53f7",
+  "euro-currency sign": "\u6b27\u5143\u7b26\u53f7",
+  "colon sign": "\u5192\u53f7",
+  "cruzeiro sign": "\u514b\u9c81\u585e\u7f57\u5e01\u7b26\u53f7",
+  "french franc sign": "\u6cd5\u90ce\u7b26\u53f7",
+  "lira sign": "\u91cc\u62c9\u7b26\u53f7",
+  "mill sign": "\u5bc6\u5c14\u7b26\u53f7",
+  "naira sign": "\u5948\u62c9\u7b26\u53f7",
+  "peseta sign": "\u6bd4\u585e\u5854\u7b26\u53f7",
+  "rupee sign": "\u5362\u6bd4\u7b26\u53f7",
+  "won sign": "\u97e9\u5143\u7b26\u53f7",
+  "new sheqel sign": "\u65b0\u8c22\u514b\u5c14\u7b26\u53f7",
+  "dong sign": "\u8d8a\u5357\u76fe\u7b26\u53f7",
+  "kip sign": "\u8001\u631d\u57fa\u666e\u7b26\u53f7",
+  "tugrik sign": "\u56fe\u683c\u91cc\u514b\u7b26\u53f7",
+  "drachma sign": "\u5fb7\u62c9\u514b\u9a6c\u7b26\u53f7",
+  "german penny symbol": "\u5fb7\u56fd\u4fbf\u58eb\u7b26\u53f7",
+  "peso sign": "\u6bd4\u7d22\u7b26\u53f7",
+  "guarani sign": "\u74dc\u62c9\u5c3c\u7b26\u53f7",
+  "austral sign": "\u6fb3\u5143\u7b26\u53f7",
+  "hryvnia sign": "\u683c\u91cc\u592b\u5c3c\u4e9a\u7b26\u53f7",
+  "cedi sign": "\u585e\u5730\u7b26\u53f7",
+  "livre tournois sign": "\u91cc\u5f17\u5c14\u7b26\u53f7",
+  "spesmilo sign": "\u65af\u4f69\u65af\u7c73\u6d1b\u7b26\u53f7",
+  "tenge sign": "\u575a\u6208\u7b26\u53f7",
+  "indian rupee sign": "\u5370\u5ea6\u5362\u6bd4\u7b26\u53f7",
+  "turkish lira sign": "\u571f\u8033\u5176\u91cc\u62c9\u7b26\u53f7",
+  "nordic mark sign": "\u5317\u6b27\u9a6c\u514b\u7b26\u53f7",
+  "manat sign": "\u9a6c\u7eb3\u7279\u7b26\u53f7",
+  "ruble sign": "\u5362\u5e03\u7b26\u53f7",
+  "yen character": "\u65e5\u5143\u5b57\u7b26",
+  "yuan character": "\u4eba\u6c11\u5e01\u5143\u5b57\u7b26",
+  "yuan character, in Hong Kong and Taiwan": "\u5143\u5b57\u7b26\uff08\u6e2f\u53f0\u5730\u533a\uff09",
+  "yen/yuan character variant one": "\u5143\u5b57\u7b26\uff08\u5927\u5199\uff09",
+  "Embed URL": "\u5d4c\u5165\u5730\u5740",
+  "Paste the embed code below:": "\u5c06\u5185\u5d4c\u4ee3\u7801\u7c98\u8d34\u5728\u4e0b\u9762:",
+  "Insert/Edit embedded media": "\u63d2\u5165/\u7f16\u8f91\u5a92\u4f53",
+  "Document properties": "\u6587\u6863\u5c5e\u6027",
+  "Robots": "\u673a\u5668\u4eba",
+  "Description": "\u63cf\u8ff0",
+  "Encoding": "\u7f16\u7801",
+  "Author": "\u4f5c\u8005",
+  "Fullscreen": "\u5168\u5c4f",
+  "Horizontal Rule": "\u6c34\u5e73\u5206\u5272\u7ebf",
+  "Horizontal line": "\u6c34\u5e73\u5206\u5272\u7ebf",
+  "Insert/edit anchor": "\u63d2\u5165/\u7f16\u8f91\u951a\u70b9",
+  "Open in new tab": "\u5728\u65b0\u6807\u7b7e\u9875\u4e2d\u6253\u5f00",
+  "Target": "\u6253\u5f00\u65b9\u5f0f",
+  "Rel": "\u5173\u7cfb",
+  "Media URL": "\u5a92\u4f53\u5730\u5740",
+  "Media type": "\u5a92\u4f53\u7c7b\u578b",
+  "Upload file": "\u4e0a\u4f20\u6587\u4ef6",
+  "You have clicked a link in read-only mode": "\u5728\u53ea\u8bfb\u6a21\u5f0f\u4e0b\u70b9\u51fb\u4e86\u94fe\u63a5",
+  "The current editor ({0}) mode is readonly. To apply the requested action, switch the editor to edit mode first.": "\u5f53\u524d\u7f16\u8f91\u5668 ({0}) \u6a21\u5f0f\u4e3a\u53ea\u8bfb\u3002\u8981\u5e94\u7528\u8bf7\u6c42\u7684\u64cd\u4f5c\uff0c\u8bf7\u5148\u5c06\u7f16\u8f91\u5668\u5207\u6362\u5230\u7f16\u8f91\u6a21\u5f0f\u3002",
+  "Version": "\u7248\u672c",
+  "Keyboard Navigation": "\u952e\u76d8\u5bfc\u822a",
+  "Color Picker": "\u989c\u8272\u9009\u62e9\u5668",
+  "Color swatch": "\u989c\u8272\u6837\u672c",
+  "Color value": "\u989c\u8272\u503c",
+  "R": "R",
+  "G": "G",
+  "B": "B",
+  "Hex": "Hex",
+  "#": "#",
+  "No color": "\u65e0\u989c\u8272",
+  "Custom color": "\u81ea\u5b9a\u4e49\u989c\u8272",
+  "Invalid hex color code: {0}": "\u65e0\u6548\u7684\u5341\u516d\u8fdb\u5236\u989c\u8272\u4ee3\u7801: {0}",
+  "Invalid input": "\u65e0\u6548\u8f93\u5165",
+  "Basic formatting": "\u57fa\u672c\u683c\u5f0f",
+  "Increase font size": "\u589e\u5927\u5b57\u53f7",
+  "Decrease font size": "\u51cf\u5c0f\u5b57\u53f7",
+  "Font size {0}": "\u5b57\u53f7 {0}",
+  "Formatting": "\u683c\u5f0f\u5316",
+  "More...": "\u66f4\u591a...",
+  "History": "\u5386\u53f2",
+  "Resize": "\u8c03\u6574\u5927\u5c0f"
+})

+ 87 - 10
code/frontend/src/views/content/index.vue

@@ -72,7 +72,8 @@
         </el-table-column>
         <el-table-column prop="watermarkText" label="水印" width="100" align="center" show-overflow-tooltip>
           <template #default="{ row }">
-            <el-tag v-if="row.watermarkText" type="success" size="small">已设置</el-tag>
+            <el-tag v-if="row.watermarkImage" type="success" size="small">图片</el-tag>
+            <el-tag v-else-if="row.watermarkText" type="success" size="small">文本</el-tag>
             <span v-else>-</span>
           </template>
         </el-table-column>
@@ -205,14 +206,40 @@
           />
         </el-form-item>
         <el-form-item label="水印">
-          <el-input
-            v-model="formData.watermarkText"
-            maxlength="50"
-            placeholder="输入水印文本,留空则不显示水印"
-            clearable
-            style="width: 320px;"
-          />
-          <span class="form-tip" style="margin-left: 12px;">填写后文章阅读页会显示该水印文本</span>
+          <div class="watermark-config">
+            <div class="watermark-text-row">
+              <el-input
+                v-model="formData.watermarkText"
+                maxlength="50"
+                placeholder="输入水印文本,留空则不显示文本水印"
+                clearable
+                style="width: 320px;"
+              />
+              <span class="form-tip" style="margin-left: 12px;">文本水印</span>
+            </div>
+            <div class="watermark-image-row">
+              <el-upload
+                :show-file-list="false"
+                :http-request="handleWatermarkUpload"
+                accept="image/*"
+                :disabled="dialogMode === 'view'"
+              >
+                <div class="watermark-uploader">
+                  <img v-if="formData.watermarkImage" :src="formData.watermarkImage" class="watermark-preview" />
+                  <el-icon v-else class="watermark-icon"><Plus /></el-icon>
+                </div>
+              </el-upload>
+              <el-button
+                v-if="formData.watermarkImage && dialogMode !== 'view'"
+                type="danger"
+                link
+                size="small"
+                style="margin-left: 8px;"
+                @click="formData.watermarkImage = ''"
+              >删除图片</el-button>
+              <span class="form-tip" style="margin-left: 12px;">图片水印(优先级高于文本),建议使用PNG透明背景图片</span>
+            </div>
+          </div>
         </el-form-item>
         <el-form-item label="内容" prop="content">
           <RichEditor
@@ -322,7 +349,8 @@ const defaultForm = () => ({
   content: '',
   status: '',
   rejectReason: '',
-  watermarkText: ''
+  watermarkText: '',
+  watermarkImage: ''
 })
 const formData = reactive(defaultForm())
 
@@ -555,6 +583,21 @@ async function handleCoverUpload({ file }) {
   }
 }
 
+/**
+ * 水印图片上传(自定义 http-request)
+ */
+async function handleWatermarkUpload({ file }) {
+  try {
+    const res = await uploadImage(file)
+    if (res?.data?.url) {
+      formData.watermarkImage = res.data.url
+      ElMessage.success('水印图片上传成功')
+    }
+  } catch (e) {
+    // 拦截器已提示
+  }
+}
+
 /**
  * 查看文章页面(跳转后端公开阅读页)
  */
@@ -679,6 +722,40 @@ onMounted(() => {
     color: #8c939d;
   }
 }
+.watermark-config {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+.watermark-text-row,
+.watermark-image-row {
+  display: flex;
+  align-items: center;
+}
+.watermark-uploader {
+  width: 80px;
+  height: 80px;
+  border: 1px dashed #d9d9d9;
+  border-radius: 6px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  transition: border-color 0.2s;
+  overflow: hidden;
+  &:hover {
+    border-color: #409eff;
+  }
+  .watermark-preview {
+    width: 100%;
+    height: 100%;
+    object-fit: contain;
+  }
+  .watermark-icon {
+    font-size: 24px;
+    color: #8c939d;
+  }
+}
 .form-tip {
   font-size: 12px;
   color: #909399;