diff --git a/core/core-backend/src/main/java/io/dataease/visualization/server/VisualizationWatermarkService.java b/core/core-backend/src/main/java/io/dataease/visualization/server/VisualizationWatermarkService.java
new file mode 100644
index 0000000000..b1f880c99c
--- /dev/null
+++ b/core/core-backend/src/main/java/io/dataease/visualization/server/VisualizationWatermarkService.java
@@ -0,0 +1,41 @@
+package io.dataease.visualization.server;
+
+import io.dataease.api.visualization.VisualizationWatermarkApi;
+import io.dataease.api.visualization.request.VisualizationWatermarkRequest;
+import io.dataease.api.visualization.vo.VisualizationWatermarkVO;
+import io.dataease.utils.BeanUtils;
+import io.dataease.visualization.dao.auto.entity.VisualizationWatermark;
+import io.dataease.visualization.dao.auto.mapper.VisualizationWatermarkMapper;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @author : WangJiaHao
+ * @date : 2024/1/10 16:59
+ */
+@RestController
+@RequestMapping("/watermark")
+public class VisualizationWatermarkService implements VisualizationWatermarkApi {
+
+    private final static String DEFAULT_ID ="system_default";
+
+    @Resource
+    private VisualizationWatermarkMapper watermarkMapper;
+
+    @Override
+    public VisualizationWatermarkVO getWatermarkInfo() {
+        VisualizationWatermark watermark =  watermarkMapper.selectById(DEFAULT_ID);
+        VisualizationWatermarkVO watermarkVO = new VisualizationWatermarkVO();
+        return BeanUtils.copyBean(watermarkVO,watermark);
+    }
+
+    @Override
+    public void saveWatermarkInfo(VisualizationWatermarkRequest watermarkRequest) {
+        VisualizationWatermark watermark =  new VisualizationWatermark();
+        BeanUtils.copyBean(watermark,watermarkRequest);
+        watermark.setId(DEFAULT_ID);
+        watermarkMapper.updateById(watermark);
+    }
+}
diff --git a/core/core-backend/src/main/resources/db/desktop/V2.3__ddl.sql b/core/core-backend/src/main/resources/db/desktop/V2.3__ddl.sql
new file mode 100644
index 0000000000..f8a9c19a0e
--- /dev/null
+++ b/core/core-backend/src/main/resources/db/desktop/V2.3__ddl.sql
@@ -0,0 +1,12 @@
+
+DROP TABLE IF EXISTS `visualization_watermark`;
+CREATE TABLE `visualization_watermark` (
+                                           `id` varchar(50) NOT NULL COMMENT '主键',
+                                           `version` varchar(255) DEFAULT NULL COMMENT '版本号',
+                                           `setting_content` longtext COMMENT '设置内容',
+                                           `create_by` varchar(255) DEFAULT NULL COMMENT '创建人',
+                                           `create_time` bigint(13) DEFAULT NULL COMMENT '创建时间',
+                                           PRIMARY KEY (`id`)
+)  COMMENT='仪表板水印设置表';
+
+INSERT INTO `visualization_watermark` (`id`, `version`, `setting_content`, `create_by`, `create_time`) VALUES ('system_default', '1.0', '{\"enable\":false,\"enablePanelCustom\":true,\"type\":\"custom\",\"content\":\"水印\",\"watermark_color\":\"#DD1010\",\"watermark_x_space\":12,\"watermark_y_space\":36,\"watermark_fontsize\":15}', 'admin', NULL);
diff --git a/core/core-backend/src/main/resources/db/migration/V2.3__ddl.sql b/core/core-backend/src/main/resources/db/migration/V2.3__ddl.sql
new file mode 100644
index 0000000000..f8a9c19a0e
--- /dev/null
+++ b/core/core-backend/src/main/resources/db/migration/V2.3__ddl.sql
@@ -0,0 +1,12 @@
+
+DROP TABLE IF EXISTS `visualization_watermark`;
+CREATE TABLE `visualization_watermark` (
+                                           `id` varchar(50) NOT NULL COMMENT '主键',
+                                           `version` varchar(255) DEFAULT NULL COMMENT '版本号',
+                                           `setting_content` longtext COMMENT '设置内容',
+                                           `create_by` varchar(255) DEFAULT NULL COMMENT '创建人',
+                                           `create_time` bigint(13) DEFAULT NULL COMMENT '创建时间',
+                                           PRIMARY KEY (`id`)
+)  COMMENT='仪表板水印设置表';
+
+INSERT INTO `visualization_watermark` (`id`, `version`, `setting_content`, `create_by`, `create_time`) VALUES ('system_default', '1.0', '{\"enable\":false,\"enablePanelCustom\":true,\"type\":\"custom\",\"content\":\"水印\",\"watermark_color\":\"#DD1010\",\"watermark_x_space\":12,\"watermark_y_space\":36,\"watermark_fontsize\":15}', 'admin', NULL);
diff --git a/core/core-frontend/src/api/watermark.ts b/core/core-frontend/src/api/watermark.ts
index 6c78457973..dacfd4c8c3 100644
--- a/core/core-frontend/src/api/watermark.ts
+++ b/core/core-frontend/src/api/watermark.ts
@@ -1,4 +1,5 @@
 import request from '@/config/axios'
 
-export const searchRoleApi = (keyword: string) =>
-  request.post({ url: '/role/query', data: { keyword } })
+export const watermarkSave = params => request.post({ url: '/watermark/save', data: params })
+
+export const watermarkFind = () => request.get({ url: 'watermark/find' })
diff --git a/core/core-frontend/src/assets/img/watermark-demo-dark.png b/core/core-frontend/src/assets/img/watermark-demo-dark.png
new file mode 100644
index 0000000000..b4a48c7334
Binary files /dev/null and b/core/core-frontend/src/assets/img/watermark-demo-dark.png differ
diff --git a/core/core-frontend/src/assets/img/watermark-demo-light.png b/core/core-frontend/src/assets/img/watermark-demo-light.png
new file mode 100644
index 0000000000..78b5b725a2
Binary files /dev/null and b/core/core-frontend/src/assets/img/watermark-demo-light.png differ
diff --git a/core/core-frontend/src/locales/zh-CN.ts b/core/core-frontend/src/locales/zh-CN.ts
index 7c29dca6a3..605dfb1fe3 100644
--- a/core/core-frontend/src/locales/zh-CN.ts
+++ b/core/core-frontend/src/locales/zh-CN.ts
@@ -2131,5 +2131,24 @@ export default {
   template_manage: {
     name_already_exists_type: '分类名称已存在',
     the_same_category: '同一分类下,该模板名称已存在'
+  },
+  watermark: {
+    support_params: '当前支持的参数:',
+    enable: '启用水印',
+    enable_panel_custom: '允许仪表板单独打开或者关闭水印',
+    content: '水印内容',
+    custom_content: '自定义公式',
+    account: '账号',
+    nick_name: '昵称',
+    ip: 'IP',
+    now: '当前时间',
+    watermark_color: '水印颜色',
+    watermark_font_size: '水印字号',
+    watermark_space: '水印间距',
+    horizontal: '横向间距',
+    vertical: '纵向间距',
+    reset: '重置',
+    preview: '预览',
+    save: '保存'
   }
 }
diff --git a/core/core-frontend/src/views/watermark/ParamsTips.vue b/core/core-frontend/src/views/watermark/ParamsTips.vue
new file mode 100644
index 0000000000..c77e6c90cd
--- /dev/null
+++ b/core/core-frontend/src/views/watermark/ParamsTips.vue
@@ -0,0 +1,33 @@
+<template>
+  <div class="tips-class">
+    <el-tooltip class="item" effect="dark" placement="bottom">
+      <template #content>
+        <div>
+          <span>{{ t('watermark.support_params') }}</span
+          ><br />
+          <span>${username}-{{ t('watermark.account') }}</span
+          ><br />
+          <span>${nickName}-{{ t('watermark.nick_name') }}</span
+          ><br />
+          <span>${time}-{{ t('watermark.now') }}</span
+          ><br />
+          <span>${ip}-IP</span><br />
+        </div>
+      </template>
+      <span><i class="el-icon-warning" /></span>
+    </el-tooltip>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useI18n } from '@/hooks/web/useI18n'
+const { t } = useI18n()
+</script>
+
+<style scoped lang="less">
+.tips-class {
+  position: absolute;
+  right: 10px;
+  z-index: 10;
+}
+</style>
diff --git a/core/core-frontend/src/views/watermark/index.vue b/core/core-frontend/src/views/watermark/index.vue
new file mode 100644
index 0000000000..0d023a5b47
--- /dev/null
+++ b/core/core-frontend/src/views/watermark/index.vue
@@ -0,0 +1,304 @@
+<template xmlns:el-col="http://www.w3.org/1999/html">
+  <p class="router-title">水印管理</p>
+  <el-row class="watermark-table__content">
+    <el-row class="watermark-main-outer">
+      <el-col class="main-col-left" :span="12">
+        <el-form ref="watermarkForm" :model="state.watermarkForm" label-width="120px" size="middle">
+          <el-form-item :label="t('watermark.enable')" style="text-align: left">
+            <el-switch
+              size="middle"
+              v-model="state.watermarkForm.enable"
+              @change="enableChange"
+            ></el-switch>
+          </el-form-item>
+          <el-form-item label="" style="text-align: left">
+            <el-checkbox
+              :disabled="!state.watermarkForm.enable"
+              v-model="state.watermarkForm.enablePanelCustom"
+            ></el-checkbox>
+            {{ t('watermark.enable_panel_custom') }}
+          </el-form-item>
+          <el-form-item :label="t('watermark.content')" style="text-align: left">
+            <el-select :disabled="!state.watermarkForm.enable" v-model="state.watermarkForm.type">
+              <el-option :label="t('watermark.custom_content')" value="custom" />
+              <el-option :label="t('watermark.account')" value="userName" />
+              <el-option :label="t('watermark.nick_name')" value="nickName" />
+              <el-option :label="t('watermark.ip')" value="ip" />
+              <el-option :label="t('watermark.now')" value="time" />
+            </el-select>
+          </el-form-item>
+
+          <el-form-item
+            label=""
+            v-show="state.watermarkForm.type === 'custom'"
+            style="text-align: left"
+          >
+            <params-tips></params-tips>
+            <el-input
+              :disabled="!state.watermarkForm.enable"
+              v-model="state.watermarkForm.content"
+              type="textarea"
+              :autosize="{ minRows: 4, maxRows: 4 }"
+            />
+          </el-form-item>
+
+          <el-form-item :label="t('watermark.watermark_color')" style="text-align: left">
+            <el-color-picker
+              :disabled="!state.watermarkForm.enable"
+              v-model="state.watermarkForm.watermark_color"
+              :predefine="state.predefineColors"
+              size="middle"
+            />
+          </el-form-item>
+          <el-form-item :label="t('watermark.watermark_font_size')" style="text-align: left">
+            <el-input-number
+              :disabled="!state.watermarkForm.enable"
+              v-model="state.watermarkForm.watermark_fontsize"
+              :min="12"
+              :max="32"
+              size="middle"
+            />
+            px
+          </el-form-item>
+          <el-form-item :label="t('watermark.horizontal')" style="text-align: left">
+            <el-input-number
+              :disabled="!state.watermarkForm.enable"
+              v-model="state.watermarkForm.watermark_x_space"
+              :min="10"
+              :max="400"
+            />
+            px
+          </el-form-item>
+          <el-form-item :label="t('watermark.vertical')" style="text-align: left">
+            <el-input-number
+              :disabled="!state.watermarkForm.enable"
+              v-model="state.watermarkForm.watermark_y_space"
+              :min="10"
+              :max="400"
+            />
+            px
+          </el-form-item>
+        </el-form>
+        <el-row style="margin-left: 53px; text-align: left">
+          <el-button size="middle" type="i" @click="cancel">{{ t('watermark.reset') }} </el-button>
+          <el-button size="middle" type="info" @click="preview"
+            >{{ t('watermark.preview') }}
+          </el-button>
+          <el-button type="primary" size="middle" @click="save"
+            >{{ t('watermark.save') }}
+          </el-button>
+        </el-row>
+      </el-col>
+      <el-col :span="12" style="height: 100%">
+        <div
+          id="watermark-demo"
+          style="position: relative; width: 100%; height: 100%; padding: 20px"
+        >
+          <div class="demo-preview">
+            <img style="height: 100%" src="@/assets/img/watermark-demo-light.png" />
+          </div>
+          <div class="demo-preview" style="margin-top: 15px">
+            <img style="height: 100%" src="@/assets/img/watermark-demo-dark.png" />
+          </div>
+        </div>
+      </el-col>
+    </el-row>
+  </el-row>
+</template>
+
+<script setup>
+import { onMounted, reactive, ref } from 'vue'
+import { watermarkFind, watermarkSave } from '@/api/watermark'
+import { ElMessage } from 'element-plus-secondary/es'
+import { personInfoApi } from '@/api/user'
+import { getNow, watermark } from '@/components/watermark/watermark'
+import { useI18n } from '@/hooks/web/useI18n'
+const { t } = useI18n()
+
+const state = reactive({
+  userLoginInfo: {
+    username: '',
+    nickName: '',
+    ip: ''
+  },
+  cmOption: {
+    tabSize: 2,
+    styleActiveLine: true,
+    lineNumbers: true,
+    line: true,
+    mode: 'text/x-textile',
+    theme: 'solarized',
+    hintOptions: {
+      // 自定义提示选项
+      completeSingle: false // 当匹配只有一项的时候是否自动补全
+    }
+  },
+  watermarkFormSource: null,
+  predefineColors: [
+    '#ff4500',
+    '#ff8c00',
+    '#ffd700',
+    '#90ee90',
+    '#00ced1',
+    '#1e90ff',
+    '#c71585',
+    '#999999',
+    '#000000',
+    '#FFFFFF'
+  ],
+  watermarkForm: {
+    enable: false,
+    enablePanelCustom: false,
+    type: 'userName',
+    content: '${time}-${ip}-${nickName}',
+    watermark_color: '#999999',
+    watermark_x_space: 100,
+    watermark_y_space: 100,
+    watermark_fontsize: 20
+  }
+})
+
+const enableChange = val => {
+  initWatermark()
+}
+
+const preview = () => {
+  initWatermark()
+}
+
+const cancel = () => {
+  state.watermarkForm = { ...state.watermarkFormSource }
+  const params = {
+    settingContent: JSON.stringify(state.watermarkForm)
+  }
+  watermarkSave(params).then(rsp => {
+    //ignore
+  })
+  initWatermark()
+}
+
+const save = () => {
+  const params = {
+    settingContent: JSON.stringify(state.watermarkForm)
+  }
+  watermarkSave(params).then(rsp => {
+    ElMessage.success('保存成功')
+  })
+}
+
+const findData = callback => {
+  watermarkFind().then(rsp => {
+    callback(rsp)
+  })
+}
+
+const findUserData = callback => {
+  personInfoApi().then(rsp => {
+    callback(rsp)
+  })
+}
+
+const initData = () => {
+  findData(res => {
+    state.watermarkForm = JSON.parse(res.data.settingContent)
+    state.watermarkFormSource = { ...state.watermarkForm }
+    initWatermark()
+  })
+}
+
+const initWatermark = () => {
+  let watermark_txt
+  let watermark_width = 120
+  if (state.watermarkForm.type === 'custom') {
+    watermark_txt = state.watermarkForm.content
+    watermark_txt = watermark_txt.replaceAll('${ip}', state.userLoginInfo.ip)
+    watermark_txt = watermark_txt.replaceAll('${username}', state.userLoginInfo.name)
+    watermark_txt = watermark_txt.replaceAll('${nickName}', state.userLoginInfo.account)
+    watermark_txt = watermark_txt.replaceAll('${time}', getNow())
+    watermark_width = watermark_txt.length * state.watermarkForm.watermark_fontsize * 0.75
+    watermark_width = watermark_width > 350 ? 350 : watermark_width
+  } else if (state.watermarkForm.type === 'nickName') {
+    watermark_txt = state.userLoginInfo.account
+  } else if (state.watermarkForm.type === 'ip') {
+    watermark_txt = state.userLoginInfo.ip
+    watermark_width = watermark_txt.length * state.watermarkForm.watermark_fontsize + 30
+  } else if (state.watermarkForm.type === 'time') {
+    watermark_txt = getNow()
+    watermark_width = 200
+  } else {
+    watermark_txt = state.userLoginInfo.name
+  }
+  const settings = {
+    watermark_enable: state.watermarkForm.enable,
+    watermark_txt: watermark_txt,
+    watermark_width: watermark_width,
+    watermark_color: state.watermarkForm.watermark_color,
+    watermark_x_space: state.watermarkForm.watermark_x_space,
+    watermark_y_space: state.watermarkForm.watermark_y_space,
+    watermark_fontsize: state.watermarkForm.watermark_fontsize + 'px'
+  }
+  // 清理历史水印
+  const historyWatermarkDom = document.getElementById('de-watermark-server')
+  if (historyWatermarkDom) {
+    historyWatermarkDom.remove()
+  }
+  if (state.watermarkForm.enable) {
+    watermark(settings, 'watermark-demo')
+  }
+}
+
+onMounted(() => {
+  findUserData(res => {
+    state.userLoginInfo = res.data
+    initData()
+  })
+})
+</script>
+
+<style lang="less" scoped>
+.demo-preview {
+  height: calc(50vh - 130px);
+  width: 100%;
+  float: left;
+  overflow-x: auto;
+  overflow-y: hidden;
+  text-align: center;
+}
+
+.main-col-left {
+  overflow-y: auto;
+  min-width: 240px;
+  padding-right: 20px;
+  padding-top: 20px;
+  height: 100%;
+  border-right: 1px solid gainsboro;
+}
+
+.watermark-main-outer {
+  border: 1px solid gainsboro;
+  height: calc(100vh - 180px);
+  min-width: 800px;
+  min-height: 500px;
+  overflow-y: hidden;
+}
+
+.router-title {
+  color: #1f2329;
+  font-feature-settings: 'clig' off, 'liga' off;
+  font-family: PingFang SC;
+  font-size: 20px;
+  font-style: normal;
+  font-weight: 500;
+  line-height: 28px;
+}
+.watermark-table__content {
+  padding: 24px;
+  width: 100%;
+  background: var(--ContentBG, #ffffff);
+  height: calc(100% - 40px) !important;
+  box-sizing: border-box;
+  margin-top: 12px;
+  overflow-x: auto;
+  border-radius: 4px;
+}
+</style>
diff --git a/sdk/api/api-base/src/main/java/io/dataease/api/visualization/VisualizationWatermarkApi.java b/sdk/api/api-base/src/main/java/io/dataease/api/visualization/VisualizationWatermarkApi.java
new file mode 100644
index 0000000000..8714d4056e
--- /dev/null
+++ b/sdk/api/api-base/src/main/java/io/dataease/api/visualization/VisualizationWatermarkApi.java
@@ -0,0 +1,27 @@
+package io.dataease.api.visualization;
+
+import com.github.xiaoymin.knife4j.annotations.ApiSupport;
+import io.dataease.api.visualization.request.VisualizationSubjectRequest;
+import io.dataease.api.visualization.request.VisualizationWatermarkRequest;
+import io.dataease.api.visualization.vo.VisualizationSubjectVO;
+import io.dataease.api.visualization.vo.VisualizationWatermarkVO;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@Tag(name = "可视化管理:水印")
+@ApiSupport(order = 994)
+public interface VisualizationWatermarkApi {
+
+
+    @ResponseBody
+    @GetMapping("/find")
+    VisualizationWatermarkVO getWatermarkInfo();
+
+    @ResponseBody
+    @PostMapping("/save")
+    void saveWatermarkInfo(@RequestBody VisualizationWatermarkRequest watermarkRequest);
+
+}
diff --git a/sdk/api/api-base/src/main/java/io/dataease/api/visualization/request/VisualizationWatermarkRequest.java b/sdk/api/api-base/src/main/java/io/dataease/api/visualization/request/VisualizationWatermarkRequest.java
new file mode 100644
index 0000000000..608c61adb8
--- /dev/null
+++ b/sdk/api/api-base/src/main/java/io/dataease/api/visualization/request/VisualizationWatermarkRequest.java
@@ -0,0 +1,10 @@
+package io.dataease.api.visualization.request;
+
+import io.dataease.api.visualization.vo.VisualizationWatermarkVO;
+
+/**
+ * @author : WangJiaHao
+ * @date : 2024/1/10 17:02
+ */
+public class VisualizationWatermarkRequest extends VisualizationWatermarkVO {
+}