refactor timi-web
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
/cache
|
||||||
|
/project.local.yml
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
# the name by which the project can be referenced within Serena
|
||||||
|
project_name: "timi-tdesign-mobile"
|
||||||
|
|
||||||
|
|
||||||
|
# list of languages for which language servers are started; choose from:
|
||||||
|
# al bash clojure cpp csharp
|
||||||
|
# csharp_omnisharp dart elixir elm erlang
|
||||||
|
# fortran fsharp go groovy haskell
|
||||||
|
# haxe java julia kotlin lua
|
||||||
|
# markdown
|
||||||
|
# matlab nix pascal perl php
|
||||||
|
# php_phpactor powershell python python_jedi r
|
||||||
|
# rego ruby ruby_solargraph rust scala
|
||||||
|
# swift terraform toml typescript typescript_vts
|
||||||
|
# vue yaml zig
|
||||||
|
# (This list may be outdated. For the current list, see values of Language enum here:
|
||||||
|
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
|
||||||
|
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
|
||||||
|
# Note:
|
||||||
|
# - For C, use cpp
|
||||||
|
# - For JavaScript, use typescript
|
||||||
|
# - For Free Pascal/Lazarus, use pascal
|
||||||
|
# Special requirements:
|
||||||
|
# Some languages require additional setup/installations.
|
||||||
|
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
|
||||||
|
# When using multiple languages, the first language server that supports a given file will be used for that file.
|
||||||
|
# The first language is the default language and the respective language server will be used as a fallback.
|
||||||
|
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
|
||||||
|
languages:
|
||||||
|
- vue
|
||||||
|
|
||||||
|
# the encoding used by text files in the project
|
||||||
|
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
||||||
|
encoding: "utf-8"
|
||||||
|
|
||||||
|
# line ending convention to use when writing source files.
|
||||||
|
# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
|
||||||
|
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
|
||||||
|
line_ending:
|
||||||
|
|
||||||
|
# The language backend to use for this project.
|
||||||
|
# If not set, the global setting from serena_config.yml is used.
|
||||||
|
# Valid values: LSP, JetBrains
|
||||||
|
# Note: the backend is fixed at startup. If a project with a different backend
|
||||||
|
# is activated post-init, an error will be returned.
|
||||||
|
language_backend:
|
||||||
|
|
||||||
|
# whether to use project's .gitignore files to ignore files
|
||||||
|
ignore_all_files_in_gitignore: true
|
||||||
|
|
||||||
|
# advanced configuration option allowing to configure language server-specific options.
|
||||||
|
# Maps the language key to the options.
|
||||||
|
# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
|
||||||
|
# No documentation on options means no options are available.
|
||||||
|
ls_specific_settings: {}
|
||||||
|
|
||||||
|
# list of additional paths to ignore in this project.
|
||||||
|
# Same syntax as gitignore, so you can use * and **.
|
||||||
|
# Note: global ignored_paths from serena_config.yml are also applied additively.
|
||||||
|
ignored_paths: []
|
||||||
|
|
||||||
|
# whether the project is in read-only mode
|
||||||
|
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||||
|
# Added on 2025-04-18
|
||||||
|
read_only: false
|
||||||
|
|
||||||
|
# list of tool names to exclude.
|
||||||
|
# This extends the existing exclusions (e.g. from the global configuration)
|
||||||
|
#
|
||||||
|
# Below is the complete list of tools for convenience.
|
||||||
|
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||||
|
# execute `uv run scripts/print_tool_overview.py`.
|
||||||
|
#
|
||||||
|
# * `activate_project`: Activates a project based on the project name or path.
|
||||||
|
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||||
|
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||||
|
# * `delete_memory`: Delete a memory file. Should only happen if a user asks for it explicitly,
|
||||||
|
# for example by saying that the information retrieved from a memory file is no longer correct
|
||||||
|
# or no longer relevant for the project.
|
||||||
|
# * `edit_memory`: Replaces content matching a regular expression in a memory.
|
||||||
|
# * `execute_shell_command`: Executes a shell command.
|
||||||
|
# * `find_file`: Finds files in the given relative paths
|
||||||
|
# * `find_referencing_symbols`: Finds symbols that reference the given symbol using the language server backend
|
||||||
|
# * `find_symbol`: Performs a global (or local) search using the language server backend.
|
||||||
|
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||||
|
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||||
|
# * `initial_instructions`: Provides instructions Serena usage (i.e. the 'Serena Instructions Manual')
|
||||||
|
# for clients that do not read the initial instructions when the MCP server is connected.
|
||||||
|
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||||
|
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||||
|
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||||
|
# * `list_memories`: List available memories. Any memory can be read using the `read_memory` tool.
|
||||||
|
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||||
|
# * `read_file`: Reads a file within the project directory.
|
||||||
|
# * `read_memory`: Read the content of a memory file. This tool should only be used if the information
|
||||||
|
# is relevant to the current task. You can infer whether the information
|
||||||
|
# is relevant from the memory file name.
|
||||||
|
# You should not read the same memory file multiple times in the same conversation.
|
||||||
|
# * `rename_memory`: Renames or moves a memory. Moving between project and global scope is supported
|
||||||
|
# (e.g., renaming "global/foo" to "bar" moves it from global to project scope).
|
||||||
|
# * `rename_symbol`: Renames a symbol throughout the codebase using language server refactoring capabilities.
|
||||||
|
# For JB, we use a separate tool.
|
||||||
|
# * `replace_content`: Replaces content in a file (optionally using regular expressions).
|
||||||
|
# * `replace_symbol_body`: Replaces the full definition of a symbol using the language server backend.
|
||||||
|
# * `safe_delete_symbol`:
|
||||||
|
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||||
|
# * `write_memory`: Write some information (utf-8-encoded) about this project that can be useful for future tasks to a memory in md format.
|
||||||
|
# The memory name should be meaningful.
|
||||||
|
excluded_tools: []
|
||||||
|
|
||||||
|
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default).
|
||||||
|
# This extends the existing inclusions (e.g. from the global configuration).
|
||||||
|
included_optional_tools: []
|
||||||
|
|
||||||
|
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
|
||||||
|
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
|
||||||
|
fixed_tools: []
|
||||||
|
|
||||||
|
# list of mode names to that are always to be included in the set of active modes
|
||||||
|
# The full set of modes to be activated is base_modes + default_modes.
|
||||||
|
# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply.
|
||||||
|
# Otherwise, this setting overrides the global configuration.
|
||||||
|
# Set this to [] to disable base modes for this project.
|
||||||
|
# Set this to a list of mode names to always include the respective modes for this project.
|
||||||
|
base_modes:
|
||||||
|
|
||||||
|
# list of mode names that are to be activated by default.
|
||||||
|
# The full set of modes to be activated is base_modes + default_modes.
|
||||||
|
# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply.
|
||||||
|
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
|
||||||
|
# This setting can, in turn, be overridden by CLI parameters (--mode).
|
||||||
|
default_modes:
|
||||||
|
|
||||||
|
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||||
|
# (contrary to the memories, which are loaded on demand).
|
||||||
|
initial_prompt: ""
|
||||||
|
|
||||||
|
# time budget (seconds) per tool call for the retrieval of additional symbol information
|
||||||
|
# such as docstrings or parameter information.
|
||||||
|
# This overrides the corresponding setting in the global configuration; see the documentation there.
|
||||||
|
# If null or missing, use the setting from the global configuration.
|
||||||
|
symbol_info_budget:
|
||||||
|
|
||||||
|
# list of regex patterns which, when matched, mark a memory entry as read‑only.
|
||||||
|
# Extends the list from the global configuration, merging the two lists.
|
||||||
|
read_only_memory_patterns: []
|
||||||
|
|
||||||
|
# list of regex patterns for memories to completely ignore.
|
||||||
|
# Matching memories will not appear in list_memories or activate_project output
|
||||||
|
# and cannot be accessed via read_memory or write_memory.
|
||||||
|
# To access ignored memory files, use the read_file tool on the raw file path.
|
||||||
|
# Extends the list from the global configuration, merging the two lists.
|
||||||
|
# Example: ["_archive/.*", "_episodes/.*"]
|
||||||
|
ignored_memory_patterns: []
|
||||||
@@ -1,5 +1,34 @@
|
|||||||
# Vue 3 + TypeScript + Vite
|
# timi-tdesign-mobile
|
||||||
|
|
||||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
Vue 3 移动端通用组件库,依赖 `timi-web` 提供基础组件和编辑器能力。
|
||||||
|
|
||||||
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
本包不依赖 `timi-web-api`。业务项目可以只安装本包使用通用组件;需要业务接口、领域类型或用户状态时,再单独安装 `timi-web-api`。
|
||||||
|
|
||||||
|
## 文章和通知组件
|
||||||
|
|
||||||
|
`ArticleEditor`、`ArticleView`、`NotifyEditor`、`NotifyView` 只约束对象的最小字段。`Article`、`Attachment` 和 `NotifyDetail` 支持携带业务自定义字段,不要求对象来自 `timi-web-api`。
|
||||||
|
|
||||||
|
上传和附件地址由业务项目通过回调注入:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<article-editor
|
||||||
|
:model-value="article"
|
||||||
|
:upload-image="uploadImage"
|
||||||
|
:upload-video="uploadVideo"
|
||||||
|
:resolve-attachment-url="resolveAttachmentUrl"
|
||||||
|
@update:model-value="article = $event"
|
||||||
|
/>
|
||||||
|
<article-view
|
||||||
|
:article="article"
|
||||||
|
:resolve-attachment-url="resolveAttachmentUrl"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
通知组件使用同样的 `uploadImage`、`uploadVideo` 和 `resolveAttachmentUrl` 回调。没有附件上传需求时,可以不传上传回调。
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
pnpm run build
|
||||||
|
```
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ import {
|
|||||||
ArticleView
|
ArticleView
|
||||||
} from "timi-tdesign-mobile";
|
} from "timi-tdesign-mobile";
|
||||||
import type { ArticleEditorExpose } from "timi-tdesign-mobile";
|
import type { ArticleEditorExpose } from "timi-tdesign-mobile";
|
||||||
import type { Article, ArticleContentType } from "timi-web/editor";
|
import type { Article, ArticleContentType } from "timi-tdesign-mobile";
|
||||||
import { DEMO_ARTICLE_CONTENT, cloneArticle, uploadDemoMedia } from "../demo-data";
|
import { DEMO_ARTICLE_CONTENT, cloneArticle, uploadDemoMedia } from "../demo-data";
|
||||||
|
|
||||||
const contentTypeOptions: Array<{ label: string; value: ArticleContentType }> = [
|
const contentTypeOptions: Array<{ label: string; value: ArticleContentType }> = [
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ import {
|
|||||||
NotifyView
|
NotifyView
|
||||||
} from "timi-tdesign-mobile";
|
} from "timi-tdesign-mobile";
|
||||||
import type { NotifyEditorExpose } from "timi-tdesign-mobile";
|
import type { NotifyEditorExpose } from "timi-tdesign-mobile";
|
||||||
import type { NotifyDetail } from "timi-web/editor";
|
import type { NotifyDetail } from "timi-tdesign-mobile";
|
||||||
import { cloneNotify, uploadDemoMedia } from "../demo-data";
|
import { cloneNotify, uploadDemoMedia } from "../demo-data";
|
||||||
|
|
||||||
const notifyValue = ref<NotifyDetail>(cloneNotify());
|
const notifyValue = ref<NotifyDetail>(cloneNotify());
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Article, NotifyDetail, UploadResult } from "timi-web/editor";
|
import type { Article, NotifyDetail, UploadResult } from "timi-tdesign-mobile";
|
||||||
|
|
||||||
/** 编辑器演示使用的固定内容,避免每个页面各自维护一份 HTML。 */
|
/** 编辑器演示使用的固定内容,避免每个页面各自维护一份 HTML。 */
|
||||||
export const DEMO_ARTICLE_CONTENT = `<h2>跨平台编辑器</h2>
|
export const DEMO_ARTICLE_CONTENT = `<h2>跨平台编辑器</h2>
|
||||||
|
|||||||
+2
-4
@@ -1,14 +1,12 @@
|
|||||||
import { createApp } from "vue";
|
import { createApp } from "vue";
|
||||||
import Root from "./Root.vue";
|
import Root from "./Root.vue";
|
||||||
|
|
||||||
import TimiWebUI, { VPopup } from "timi-web";
|
import TimiWebUI from "timi-web";
|
||||||
|
|
||||||
import "timi-web/style.css";
|
import "timi-web/style.css";
|
||||||
import "tdesign-mobile-vue/es/style/index.css";
|
import "tdesign-mobile-vue/es/style/index.css";
|
||||||
import "../src/assets/style/tencent-cloud.less";
|
import "../src/assets/style/tdesign-custom.less";
|
||||||
import "../src/assets/style/tencent-cloud-custom.less";
|
|
||||||
|
|
||||||
const app = createApp(Root);
|
const app = createApp(Root);
|
||||||
app.use(TimiWebUI);
|
app.use(TimiWebUI);
|
||||||
app.directive("popup", VPopup);
|
|
||||||
app.mount("#root");
|
app.mount("#root");
|
||||||
|
|||||||
+4
-4
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "timi-tdesign-mobile",
|
"name": "timi-tdesign-mobile",
|
||||||
"main": "./dist/timi-tdesign-mobile.umd.js",
|
"main": "./dist/timi-tdesign-mobile.umd.js",
|
||||||
"types": "./dist/src/index.d.ts",
|
"types": "./dist/timi-tdesign-mobile/src/index.d.ts",
|
||||||
"module": "./dist/timi-tdesign-mobile.mjs",
|
"module": "./dist/timi-tdesign-mobile.mjs",
|
||||||
"style": "./dist/timi-tdesign-mobile.css",
|
"style": "./dist/timi-tdesign-mobile.css",
|
||||||
"private": false,
|
"private": false,
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
],
|
],
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/src/index.d.ts",
|
"types": "./dist/timi-tdesign-mobile/src/index.d.ts",
|
||||||
"import": "./dist/timi-tdesign-mobile.mjs",
|
"import": "./dist/timi-tdesign-mobile.mjs",
|
||||||
"require": "./dist/timi-tdesign-mobile.umd.js"
|
"require": "./dist/timi-tdesign-mobile.umd.js"
|
||||||
},
|
},
|
||||||
@@ -37,8 +37,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tdesign-mobile-vue": "^1.13.2",
|
"tdesign-mobile-vue": "^1.13.2",
|
||||||
"timi-web": "0.0.24",
|
"timi-web": "link:E:/WebProject/timi-web",
|
||||||
"vue": "^3.5.13",
|
"vue": "3.5.26",
|
||||||
"vue-router": "4.5.0"
|
"vue-router": "4.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
Generated
+385
-548
File diff suppressed because it is too large
Load Diff
@@ -50,7 +50,3 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.t-popup {
|
|
||||||
transition: all 520ms var(--tui-bezier) !important;
|
|
||||||
}
|
|
||||||
@@ -1,284 +0,0 @@
|
|||||||
:root, :root[theme-mode="light"] {
|
|
||||||
--td-brand-color-1: #fff0f2;
|
|
||||||
--td-brand-color-2: #ffdae1;
|
|
||||||
--td-brand-color-3: #ffb2c3;
|
|
||||||
--td-brand-color-4: #ff7a9b;
|
|
||||||
--td-brand-color-5: #e16283;
|
|
||||||
--td-brand-color-6: #bd4a69;
|
|
||||||
--td-brand-color-7: #9a3652;
|
|
||||||
--td-brand-color-8: #7b223d;
|
|
||||||
--td-brand-color-9: #600e2a;
|
|
||||||
--td-brand-color-10: #470019;
|
|
||||||
--td-warning-color-1: #fff1e9;
|
|
||||||
--td-warning-color-2: #ffd9c2;
|
|
||||||
--td-warning-color-3: #ffb98c;
|
|
||||||
--td-warning-color-4: #fa9550;
|
|
||||||
--td-warning-color-5: #e37318;
|
|
||||||
--td-warning-color-6: #be5a00;
|
|
||||||
--td-warning-color-7: #954500;
|
|
||||||
--td-warning-color-8: #713300;
|
|
||||||
--td-warning-color-9: #532300;
|
|
||||||
--td-warning-color-10: #3b1700;
|
|
||||||
--td-error-color-1: #fff0ed;
|
|
||||||
--td-error-color-2: #ffd8d2;
|
|
||||||
--td-error-color-3: #ffb9b0;
|
|
||||||
--td-error-color-4: #ff9285;
|
|
||||||
--td-error-color-5: #f6685d;
|
|
||||||
--td-error-color-6: #d54941;
|
|
||||||
--td-error-color-7: #ad352f;
|
|
||||||
--td-error-color-8: #881f1c;
|
|
||||||
--td-error-color-9: #68070a;
|
|
||||||
--td-error-color-10: #490002;
|
|
||||||
--td-success-color-1: #e3f9e9;
|
|
||||||
--td-success-color-2: #c6f3d7;
|
|
||||||
--td-success-color-3: #92dab2;
|
|
||||||
--td-success-color-4: #56c08d;
|
|
||||||
--td-success-color-5: #2ba471;
|
|
||||||
--td-success-color-6: #008858;
|
|
||||||
--td-success-color-7: #006c45;
|
|
||||||
--td-success-color-8: #005334;
|
|
||||||
--td-success-color-9: #003b23;
|
|
||||||
--td-success-color-10: #002515;
|
|
||||||
--td-gray-color-1: #f6f6f6;
|
|
||||||
--td-gray-color-2: #eeeceb;
|
|
||||||
--td-gray-color-3: #dddbdb;
|
|
||||||
--td-gray-color-4: #cac9c9;
|
|
||||||
--td-gray-color-5: #b6b6b6;
|
|
||||||
--td-gray-color-6: #a2a2a2;
|
|
||||||
--td-gray-color-7: #8f8f8f;
|
|
||||||
--td-gray-color-8: #7b7c7c;
|
|
||||||
--td-gray-color-9: #6a6b6b;
|
|
||||||
--td-gray-color-10: #585a5a;
|
|
||||||
--td-gray-color-11: #494a4a;
|
|
||||||
--td-gray-color-12: #3a3b3b;
|
|
||||||
--td-gray-color-13: #2c2e2e;
|
|
||||||
--td-gray-color-14: #202222;
|
|
||||||
--td-font-white-1: rgba(255, 255, 255, 1);
|
|
||||||
--td-font-white-2: rgba(255, 255, 255, .55);
|
|
||||||
--td-font-white-3: rgba(255, 255, 255, .35);
|
|
||||||
--td-font-white-4: rgba(255, 255, 255, .22);
|
|
||||||
--td-font-gray-1: rgba(0, 0, 0, .9);
|
|
||||||
--td-font-gray-2: rgba(0, 0, 0, .6);
|
|
||||||
--td-font-gray-3: rgba(0, 0, 0, .4);
|
|
||||||
--td-font-gray-4: rgba(0, 0, 0, .26);
|
|
||||||
--td-brand-color: var(--td-brand-color-4);
|
|
||||||
--td-warning-color: var(--td-warning-color-5);
|
|
||||||
--td-error-color: var(--td-error-color-6);
|
|
||||||
--td-success-color: var(--td-success-color-5);
|
|
||||||
--td-brand-color-focus: var(--td-brand-color-1);
|
|
||||||
--td-brand-color-active: var(--td-brand-color-5);
|
|
||||||
--td-brand-color-disabled: var(--td-brand-color-3);
|
|
||||||
--td-brand-color-light: var(--td-brand-color-1);
|
|
||||||
--td-brand-color-light-active: var(--td-brand-color-2);
|
|
||||||
--td-warning-color-active: var(--td-warning-color-6);
|
|
||||||
--td-warning-color-disabled: var(--td-warning-color-3);
|
|
||||||
--td-warning-color-focus: var(--td-warning-color-2);
|
|
||||||
--td-warning-color-light: var(--td-warning-color-1);
|
|
||||||
--td-warning-color-light-active: var(--td-warning-color-2);
|
|
||||||
--td-error-color-focus: var(--td-error-color-2);
|
|
||||||
--td-error-color-active: var(--td-error-color-7);
|
|
||||||
--td-error-color-disabled: var(--td-error-color-3);
|
|
||||||
--td-error-color-light: var(--td-error-color-1);
|
|
||||||
--td-error-color-light-active: var(--td-error-color-2);
|
|
||||||
--td-success-color-focus: var(--td-success-color-2);
|
|
||||||
--td-success-color-active: var(--td-success-color-6);
|
|
||||||
--td-success-color-disabled: var(--td-success-color-3);
|
|
||||||
--td-success-color-light: var(--td-success-color-1);
|
|
||||||
--td-success-color-light-active: var(--td-success-color-2);
|
|
||||||
--td-mask-active: rgba(0, 0, 0, .6);
|
|
||||||
--td-mask-disabled: rgba(255, 255, 255, .6);
|
|
||||||
--td-bg-color-page: var(--td-gray-color-1);
|
|
||||||
--td-bg-color-container: var(--td-font-white-1);
|
|
||||||
--td-bg-color-container-active: var(--td-gray-color-3);
|
|
||||||
--td-bg-color-secondarycontainer: var(--td-gray-color-1);
|
|
||||||
--td-bg-color-secondarycontainer-active: var(--td-gray-color-4);
|
|
||||||
--td-bg-color-component: var(--td-gray-color-3);
|
|
||||||
--td-bg-color-component-active: var(--td-gray-color-6);
|
|
||||||
--td-bg-color-component-disabled: var(--td-gray-color-2);
|
|
||||||
--td-bg-color-secondarycomponent: var(--td-gray-color-4);
|
|
||||||
--td-bg-color-secondarycomponent-active: var(--td-gray-color-6);
|
|
||||||
--td-bg-color-specialcomponent: #fff;
|
|
||||||
--td-text-color-primary: var(--td-font-gray-1);
|
|
||||||
--td-text-color-secondary: var(--td-font-gray-2);
|
|
||||||
--td-text-color-placeholder: var(--td-font-gray-3);
|
|
||||||
--td-text-color-disabled: var(--td-font-gray-4);
|
|
||||||
--td-text-color-anti: var(--td-font-white-1);
|
|
||||||
--td-text-color-brand: var(--td-brand-color);
|
|
||||||
--td-text-color-link: var(--td-brand-color);
|
|
||||||
--td-border-level-1-color: var(--td-gray-color-3);
|
|
||||||
--td-component-stroke: var(--td-gray-color-3);
|
|
||||||
--td-border-level-2-color: var(--td-gray-color-4);
|
|
||||||
--td-component-border: var(--td-gray-color-4);
|
|
||||||
--td-shadow-1: 0 1px 10px rgba(0, 0, 0, 5%), 0 4px 5px rgba(0, 0, 0, 8%), 0 2px 4px -1px rgba(0, 0, 0, 12%);
|
|
||||||
--td-shadow-2: 0 3px 14px 2px rgba(0, 0, 0, 5%), 0 8px 10px 1px rgba(0, 0, 0, 6%), 0 5px 5px -3px rgba(0, 0, 0, 10%);
|
|
||||||
--td-shadow-3: 0 6px 30px 5px rgba(0, 0, 0, 5%), 0 16px 24px 2px rgba(0, 0, 0, 4%), 0 8px 10px -5px rgba(0, 0, 0, 8%);
|
|
||||||
--td-shadow-4: 0 2px 8px 0 rgba(0, 0, 0, 6%);
|
|
||||||
--td-shadow-inset-top: inset 0 .5px 0 #dcdcdc;
|
|
||||||
--td-shadow-inset-right: inset .5px 0 0 #dcdcdc;
|
|
||||||
--td-shadow-inset-bottom: inset 0 -.5px 0 #dcdcdc;
|
|
||||||
--td-shadow-inset-left: inset -.5px 0 0 #dcdcdc;
|
|
||||||
--td-table-shadow-color: rgba(0, 0, 0, .08);
|
|
||||||
--td-scrollbar-color: rgba(0, 0, 0, .1);
|
|
||||||
--td-scrollbar-hover-color: rgba(0, 0, 0, .3);
|
|
||||||
--td-scroll-track-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root.dark, :root[theme-mode="dark"] {
|
|
||||||
--td-brand-color-1: #470019;
|
|
||||||
--td-brand-color-2: #600e2a;
|
|
||||||
--td-brand-color-3: #7b223d;
|
|
||||||
--td-brand-color-4: #9a3652;
|
|
||||||
--td-brand-color-5: #bd4a69;
|
|
||||||
--td-brand-color-6: #e16283;
|
|
||||||
--td-brand-color-7: #ff7a9b;
|
|
||||||
--td-brand-color-8: #ffb2c3;
|
|
||||||
--td-brand-color-9: #ffdae1;
|
|
||||||
--td-brand-color-10: #fff0f2;
|
|
||||||
--td-warning-color-1: #4f2a1d;
|
|
||||||
--td-warning-color-2: #582f21;
|
|
||||||
--td-warning-color-3: #733c23;
|
|
||||||
--td-warning-color-4: #a75d2b;
|
|
||||||
--td-warning-color-5: #cf6e2d;
|
|
||||||
--td-warning-color-6: #dc7633;
|
|
||||||
--td-warning-color-7: #e8935c;
|
|
||||||
--td-warning-color-8: #ecbf91;
|
|
||||||
--td-warning-color-9: #eed7bf;
|
|
||||||
--td-warning-color-10: #f3e9dc;
|
|
||||||
--td-error-color-1: #472324;
|
|
||||||
--td-error-color-2: #5e2a2d;
|
|
||||||
--td-error-color-3: #703439;
|
|
||||||
--td-error-color-4: #83383e;
|
|
||||||
--td-error-color-5: #a03f46;
|
|
||||||
--td-error-color-6: #c64751;
|
|
||||||
--td-error-color-7: #de6670;
|
|
||||||
--td-error-color-8: #ec888e;
|
|
||||||
--td-error-color-9: #edb1b6;
|
|
||||||
--td-error-color-10: #eeced0;
|
|
||||||
--td-success-color-1: #193a2a;
|
|
||||||
--td-success-color-2: #1a4230;
|
|
||||||
--td-success-color-3: #17533d;
|
|
||||||
--td-success-color-4: #0d7a55;
|
|
||||||
--td-success-color-5: #059465;
|
|
||||||
--td-success-color-6: #43af8a;
|
|
||||||
--td-success-color-7: #46bf96;
|
|
||||||
--td-success-color-8: #80d2b6;
|
|
||||||
--td-success-color-9: #b4e1d3;
|
|
||||||
--td-success-color-10: #deede8;
|
|
||||||
--td-gray-color-1: #f6f6f6;
|
|
||||||
--td-gray-color-2: #eeeceb;
|
|
||||||
--td-gray-color-3: #dddbdb;
|
|
||||||
--td-gray-color-4: #cac9c9;
|
|
||||||
--td-gray-color-5: #b6b6b6;
|
|
||||||
--td-gray-color-6: #a2a2a2;
|
|
||||||
--td-gray-color-7: #8f8f8f;
|
|
||||||
--td-gray-color-8: #7b7c7c;
|
|
||||||
--td-gray-color-9: #6a6b6b;
|
|
||||||
--td-gray-color-10: #585a5a;
|
|
||||||
--td-gray-color-11: #494a4a;
|
|
||||||
--td-gray-color-12: #3a3b3b;
|
|
||||||
--td-gray-color-13: #2c2e2e;
|
|
||||||
--td-gray-color-14: #202222;
|
|
||||||
--td-font-white-1: rgba(255, 255, 255, .9);
|
|
||||||
--td-font-white-2: rgba(255, 255, 255, .55);
|
|
||||||
--td-font-white-3: rgba(255, 255, 255, .35);
|
|
||||||
--td-font-white-4: rgba(255, 255, 255, .22);
|
|
||||||
--td-font-gray-1: rgba(0, 0, 0, .9);
|
|
||||||
--td-font-gray-2: rgba(0, 0, 0, .6);
|
|
||||||
--td-font-gray-3: rgba(0, 0, 0, .4);
|
|
||||||
--td-font-gray-4: rgba(0, 0, 0, .26);
|
|
||||||
--td-brand-color: var(--td-brand-color-6);
|
|
||||||
--td-warning-color: var(--td-warning-color-5);
|
|
||||||
--td-error-color: var(--td-error-color-6);
|
|
||||||
--td-success-color: var(--td-success-color-5);
|
|
||||||
--td-brand-color-focus: var(--td-brand-color-1);
|
|
||||||
--td-brand-color-active: var(--td-brand-color-7);
|
|
||||||
--td-brand-color-disabled: var(--td-brand-color-3);
|
|
||||||
--td-brand-color-light: var(--td-brand-color-1);
|
|
||||||
--td-brand-color-light-active: var(--td-brand-color-2);
|
|
||||||
--td-warning-color-focus: var(--td-warning-color-2);
|
|
||||||
--td-warning-color-active: var(--td-warning-color-4);
|
|
||||||
--td-warning-color-disabled: var(--td-warning-color-3);
|
|
||||||
--td-warning-color-light: var(--td-warning-color-1);
|
|
||||||
--td-warning-color-light-active: var(--td-warning-color-2);
|
|
||||||
--td-error-color-focus: var(--td-error-color-2);
|
|
||||||
--td-error-color-active: var(--td-error-color-5);
|
|
||||||
--td-error-color-disabled: var(--td-error-color-3);
|
|
||||||
--td-error-color-light: var(--td-error-color-1);
|
|
||||||
--td-error-color-light-active: var(--td-error-color-2);
|
|
||||||
--td-success-color-focus: var(--td-success-color-2);
|
|
||||||
--td-success-color-active: var(--td-success-color-4);
|
|
||||||
--td-success-color-disabled: var(--td-success-color-3);
|
|
||||||
--td-success-color-light: var(--td-success-color-1);
|
|
||||||
--td-success-color-light-active: var(--td-success-color-2);
|
|
||||||
--td-mask-active: rgba(0, 0, 0, .4);
|
|
||||||
--td-mask-disabled: rgba(0, 0, 0, .6);
|
|
||||||
--td-bg-color-page: var(--td-gray-color-14);
|
|
||||||
--td-bg-color-container: var(--td-gray-color-13);
|
|
||||||
--td-bg-color-secondarycontainer: var(--td-gray-color-12);
|
|
||||||
--td-bg-color-component: var(--td-gray-color-11);
|
|
||||||
--td-bg-color-container-active: var(--td-gray-color-12);
|
|
||||||
--td-bg-color-secondarycontainer-active: var(--td-gray-color-11);
|
|
||||||
--td-bg-color-component-active: var(--td-gray-color-10);
|
|
||||||
--td-bg-color-component-disabled: var(--td-gray-color-12);
|
|
||||||
--td-bg-color-specialcomponent: transparent;
|
|
||||||
--td-text-color-primary: var(--td-font-white-1);
|
|
||||||
--td-text-color-secondary: var(--td-font-white-2);
|
|
||||||
--td-text-color-placeholder: var(--td-font-white-3);
|
|
||||||
--td-text-color-disabled: var(--td-font-white-4);
|
|
||||||
--td-text-color-anti: var(--td-font-white-1);
|
|
||||||
--td-text-color-brand: var(--td-brand-color-8);
|
|
||||||
--td-text-color-link: var(--td-brand-color-8);
|
|
||||||
--td-border-level-1-color: var(--td-gray-color-11);
|
|
||||||
--td-component-stroke: var(--td-gray-color-11);
|
|
||||||
--td-border-level-2-color: var(--td-gray-color-9);
|
|
||||||
--td-component-border: var(--td-gray-color-9);
|
|
||||||
--td-shadow-1: 0 4px 6px rgba(0, 0, 0, 6%), 0 1px 10px rgba(0, 0, 0, 8%), 0 2px 4px rgba(0, 0, 0, 12%);
|
|
||||||
--td-shadow-2: 0 8px 10px rgba(0, 0, 0, 12%), 0 3px 14px rgba(0, 0, 0, 10%), 0 5px 5px rgba(0, 0, 0, 16%);
|
|
||||||
--td-shadow-3: 0 16px 24px rgba(0, 0, 0, 14%), 0 6px 30px rgba(0, 0, 0, 12%), 0 8px 10px rgba(0, 0, 0, 20%);
|
|
||||||
--td-shadow-inset-top: inset 0 .5px 0 #5e5e5e;
|
|
||||||
--td-shadow-inset-right: inset .5px 0 0 #5e5e5e;
|
|
||||||
--td-shadow-inset-bottom: inset 0 -.5px 0 #5e5e5e;
|
|
||||||
--td-shadow-inset-left: inset -.5px 0 0 #5e5e5e;
|
|
||||||
--td-table-shadow-color: rgba(0, 0, 0, .55);
|
|
||||||
--td-scrollbar-color: rgba(255, 255, 255, .1);
|
|
||||||
--td-scroll-track-color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--td-font-family: pingfang sc, microsoft yahei, arial regular;
|
|
||||||
--td-font-family-medium: pingfang sc, microsoft yahei, arial medium;
|
|
||||||
--td-font-size-link-small: 12px;
|
|
||||||
--td-font-size-link-medium: 14px;
|
|
||||||
--td-font-size-link-large: 16px;
|
|
||||||
--td-font-size-mark-extraSmall: 10px;
|
|
||||||
--td-font-size-mark-small: 12px;
|
|
||||||
--td-font-size-mark-medium: 14px;
|
|
||||||
--td-font-size-mark-large: 16px;
|
|
||||||
--td-font-size-body-extraSmall: 10px;
|
|
||||||
--td-font-size-body-small: 12px;
|
|
||||||
--td-font-size-body-medium: 14px;
|
|
||||||
--td-font-size-body-large: 16px;
|
|
||||||
--td-font-size-title-small: 14px;
|
|
||||||
--td-font-size-title-medium: 16px;
|
|
||||||
--td-font-size-title-large: 18px;
|
|
||||||
--td-font-size-title-extraLarge: 20px;
|
|
||||||
--td-font-size-headline-small: 24px;
|
|
||||||
--td-font-size-headline-medium: 28px;
|
|
||||||
--td-font-size-headline-large: 36px;
|
|
||||||
--td-font-size-display-medium: 48px;
|
|
||||||
--td-font-size-display-large: 64px;
|
|
||||||
--td-font-size: 10px;
|
|
||||||
--td-font-size-xs: var(--td-font-size-body-extraSmall);
|
|
||||||
--td-font-size-s: var(--td-font-size-body-small);
|
|
||||||
--td-font-size-base: var(--td-font-size-title-small);
|
|
||||||
--td-font-size-m: var(--td-font-size-title-medium);
|
|
||||||
--td-font-size-l: var(--td-font-size-title-large);
|
|
||||||
--td-font-size-xl: var(--td-font-size-title-extraLarge);
|
|
||||||
--td-font-size-xxl: var(--td-font-size-headline-large);
|
|
||||||
--td-radius-small: 3px;
|
|
||||||
--td-radius-default: 4px;
|
|
||||||
--td-radius-large: 12px;
|
|
||||||
--td-radius-extraLarge: 16px;
|
|
||||||
--td-radius-round: 999px;
|
|
||||||
--td-radius-circle: 50%;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import {
|
||||||
|
ATTACHMENT_ID_ATTR,
|
||||||
|
TEMP_FILE_ID_ATTR,
|
||||||
|
isLocalMediaId,
|
||||||
|
normalizeContent
|
||||||
|
} from "timi-web/editor";
|
||||||
|
import type { ArticleContentType, Attachment } from "./types";
|
||||||
|
|
||||||
|
/** 文章内容中附件和临时文件的标识前缀。 */
|
||||||
|
export const ATTACHMENT_ID_PREFIX = "attachment-id:";
|
||||||
|
export const TEMP_FILE_ID_PREFIX = "temp-file-id:";
|
||||||
|
|
||||||
|
const DEFAULT_ARTICLE_CONTENT_TYPE: ArticleContentType = "TIP_TAP";
|
||||||
|
const ARTICLE_CONTENT_TYPE_LIST: ArticleContentType[] = ["TEXT", "MARKDOWN", "TIP_TAP"];
|
||||||
|
|
||||||
|
/** 将未知正文类型归一化为组件支持的类型。 */
|
||||||
|
export function normalizeArticleContentType(contentType?: string): ArticleContentType {
|
||||||
|
return ARTICLE_CONTENT_TYPE_LIST.includes(contentType as ArticleContentType)
|
||||||
|
? contentType as ArticleContentType
|
||||||
|
: DEFAULT_ARTICLE_CONTENT_TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将空正文转换为空字符串。 */
|
||||||
|
export function normalizeRawArticleContent(content?: string): string {
|
||||||
|
return content || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从媒体地址中读取附件 ID。 */
|
||||||
|
export function getAttachmentIdFromSrc(src?: string): string {
|
||||||
|
if (!src?.startsWith(ATTACHMENT_ID_PREFIX)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return src.slice(ATTACHMENT_ID_PREFIX.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从媒体地址中读取临时文件 ID。 */
|
||||||
|
export function getTempFileIdFromSrc(src?: string): string {
|
||||||
|
if (!src?.startsWith(TEMP_FILE_ID_PREFIX)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return src.slice(TEMP_FILE_ID_PREFIX.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 构造带有附件 ID 的内部媒体地址。 */
|
||||||
|
function buildAttachmentIdSrc(attachmentId: string): string {
|
||||||
|
return `${ATTACHMENT_ID_PREFIX}${attachmentId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 仅为本地预览地址保留预览元数据。 */
|
||||||
|
function buildAttachmentMetadata(src: string): Record<string, string> | undefined {
|
||||||
|
if (!src.startsWith("blob:")) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
previewUrl: src
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 过滤前端本地占位 ID。 */
|
||||||
|
function normalizeAttachmentTempFileId(tempFileId?: string): string | undefined {
|
||||||
|
if (!tempFileId || isLocalMediaId(tempFileId)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return tempFileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从正文中提取媒体附件,返回组件需要的最小对象。 */
|
||||||
|
export function buildAttachmentListFromContent(content?: string, sourceList?: readonly Attachment[]): Attachment[] {
|
||||||
|
const rawContent = normalizeContent(content);
|
||||||
|
if (!rawContent) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(rawContent, "text/html");
|
||||||
|
const sourceMap = new Map<string, Attachment>();
|
||||||
|
for (const item of sourceList || []) {
|
||||||
|
if (item.id) {
|
||||||
|
sourceMap.set(item.id, item);
|
||||||
|
}
|
||||||
|
if (item.tempFileId) {
|
||||||
|
sourceMap.set(item.tempFileId, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const attachmentMap = new Map<string, Attachment>();
|
||||||
|
|
||||||
|
for (const media of Array.from(doc.querySelectorAll("img, video"))) {
|
||||||
|
const src = media.getAttribute("src") || "";
|
||||||
|
const attachmentId = media.getAttribute(ATTACHMENT_ID_ATTR) || getAttachmentIdFromSrc(src);
|
||||||
|
const tempFileId = media.getAttribute(TEMP_FILE_ID_ATTR) || getTempFileIdFromSrc(src);
|
||||||
|
const key = attachmentId || tempFileId;
|
||||||
|
if (!key || attachmentMap.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const sourceAttachment = sourceMap.get(key);
|
||||||
|
const previewUrl = buildAttachmentMetadata(src);
|
||||||
|
attachmentMap.set(key, {
|
||||||
|
...(sourceAttachment || {}),
|
||||||
|
id: attachmentId || sourceAttachment?.id || key,
|
||||||
|
tempFileId: normalizeAttachmentTempFileId(tempFileId) || normalizeAttachmentTempFileId(sourceAttachment?.tempFileId),
|
||||||
|
name: media.getAttribute("title") || media.getAttribute("alt") || sourceAttachment?.name || "",
|
||||||
|
metadata: previewUrl || sourceAttachment?.metadata
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(attachmentMap.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将媒体地址转换为组件统一的附件标识地址。 */
|
||||||
|
export function normalizeContentForStore(content?: string): string {
|
||||||
|
const rawContent = normalizeContent(content);
|
||||||
|
if (!rawContent) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(rawContent, "text/html");
|
||||||
|
for (const media of Array.from(doc.querySelectorAll("img, video"))) {
|
||||||
|
const src = media.getAttribute("src") || "";
|
||||||
|
const attachmentId = media.getAttribute(ATTACHMENT_ID_ATTR) || getAttachmentIdFromSrc(src);
|
||||||
|
const tempFileId = media.getAttribute(TEMP_FILE_ID_ATTR) || getTempFileIdFromSrc(src);
|
||||||
|
const finalAttachmentId = attachmentId || tempFileId;
|
||||||
|
if (!finalAttachmentId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
media.setAttribute(ATTACHMENT_ID_ATTR, finalAttachmentId);
|
||||||
|
media.removeAttribute(TEMP_FILE_ID_ATTR);
|
||||||
|
media.setAttribute("src", buildAttachmentIdSrc(finalAttachmentId));
|
||||||
|
}
|
||||||
|
return normalizeContent(doc.body.innerHTML);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 比较附件列表的通用字段,不比较对象引用。 */
|
||||||
|
export function sameAttachmentList(left?: readonly Attachment[], right?: readonly Attachment[]): boolean {
|
||||||
|
const leftList = left || [];
|
||||||
|
const rightList = right || [];
|
||||||
|
if (leftList.length !== rightList.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < leftList.length; i += 1) {
|
||||||
|
const leftItem = leftList[i];
|
||||||
|
const rightItem = rightList[i];
|
||||||
|
if (!rightItem || leftItem.id !== rightItem.id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ((leftItem.tempFileId || "") !== (rightItem.tempFileId || "")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ((leftItem.accessKey?.value || "") !== (rightItem.accessKey?.value || "")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
import view from "./index.vue";
|
import view from "./index.vue";
|
||||||
import { Toolkit } from "timi-web";
|
import { Toolkit } from "timi-web";
|
||||||
import type { Article } from "timi-web/editor";
|
import type { Article } from "./types";
|
||||||
|
|
||||||
|
export type {
|
||||||
|
Article,
|
||||||
|
ArticleContentType,
|
||||||
|
Attachment,
|
||||||
|
UploadResult
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
export type ArticleEditorExpose = {
|
export type ArticleEditorExpose = {
|
||||||
submit: () => Promise<Article>;
|
submit: () => Promise<Article>;
|
||||||
|
|||||||
@@ -24,22 +24,23 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Article, ArticleContentType, Attachment, UploadResult } from "timi-web/editor";
|
import type { Article, ArticleContentType, Attachment, UploadResult } from "./types";
|
||||||
import { AttachmentAPI } from "timi-web/editor";
|
|
||||||
import { MarkdownEditor } from "timi-web";
|
import { MarkdownEditor } from "timi-web";
|
||||||
import { TiptapEditor } from "../tiptap-editor";
|
import { TiptapEditor } from "../tiptap-editor";
|
||||||
import type { TiptapEditorExpose } from "../tiptap-editor";
|
import type { TiptapEditorExpose } from "../tiptap-editor";
|
||||||
import {
|
import {
|
||||||
ATTACHMENT_ID_ATTR,
|
|
||||||
TEMP_FILE_ID_ATTR,
|
|
||||||
buildAttachmentListFromContent,
|
buildAttachmentListFromContent,
|
||||||
getAttachmentIdFromSrc,
|
getAttachmentIdFromSrc,
|
||||||
getTempFileIdFromSrc,
|
getTempFileIdFromSrc,
|
||||||
normalizeArticleContentType,
|
normalizeArticleContentType,
|
||||||
normalizeContent,
|
|
||||||
normalizeContentForStore,
|
normalizeContentForStore,
|
||||||
normalizeRawArticleContent,
|
normalizeRawArticleContent,
|
||||||
sameAttachmentList
|
sameAttachmentList
|
||||||
|
} from "./content";
|
||||||
|
import {
|
||||||
|
ATTACHMENT_ID_ATTR,
|
||||||
|
TEMP_FILE_ID_ATTR,
|
||||||
|
normalizeContent
|
||||||
} from "timi-web/editor";
|
} from "timi-web/editor";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@@ -83,39 +84,18 @@ const attachmentMap = computed(() => {
|
|||||||
return new Map((props.modelValue?.attachmentList || []).map(item => [item.id, item]));
|
return new Map((props.modelValue?.attachmentList || []).map(item => [item.id, item]));
|
||||||
});
|
});
|
||||||
|
|
||||||
async function defaultUploadImage(file: File) {
|
|
||||||
return defaultUploadMedia(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function defaultUploadVideo(file: File) {
|
|
||||||
return defaultUploadMedia(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function defaultUploadMedia(file: File) {
|
|
||||||
const tempFileList = await AttachmentAPI.upload([file]);
|
|
||||||
const tempFileId = tempFileList[0]?.id || "";
|
|
||||||
if (!tempFileId) {
|
|
||||||
throw new Error("上传临时文件失败");
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
id: tempFileId,
|
|
||||||
url: AttachmentAPI.getTempReadURL(tempFileId),
|
|
||||||
name: file.name
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleUploadImage(file: File) {
|
async function handleUploadImage(file: File) {
|
||||||
if (props.uploadImage) {
|
if (!props.uploadImage) {
|
||||||
return props.uploadImage(file);
|
throw new Error("请为 ArticleEditor 配置 uploadImage");
|
||||||
}
|
}
|
||||||
return defaultUploadImage(file);
|
return await props.uploadImage(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleUploadVideo(file: File) {
|
async function handleUploadVideo(file: File) {
|
||||||
if (props.uploadVideo) {
|
if (!props.uploadVideo) {
|
||||||
return props.uploadVideo(file);
|
throw new Error("请为 ArticleEditor 配置 uploadVideo");
|
||||||
}
|
}
|
||||||
return defaultUploadVideo(file);
|
return await props.uploadVideo(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveAttachmentPreviewUrl(attachmentId: string) {
|
async function resolveAttachmentPreviewUrl(attachmentId: string) {
|
||||||
@@ -126,18 +106,20 @@ async function resolveAttachmentPreviewUrl(attachmentId: string) {
|
|||||||
return customUrl;
|
return customUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const previewUrl = `${attachment?.metadata?.previewUrl || ""}`;
|
const previewUrl = typeof attachment?.metadata?.previewUrl === "string"
|
||||||
|
? attachment.metadata.previewUrl
|
||||||
|
: "";
|
||||||
if (previewUrl) {
|
if (previewUrl) {
|
||||||
return previewUrl;
|
return previewUrl;
|
||||||
}
|
}
|
||||||
return attachmentId ? AttachmentAPI.getReadURL(attachmentId) : "";
|
return attachment?.url || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveTempPreviewUrl(tempFileId: string) {
|
async function resolveTempPreviewUrl(tempFileId: string) {
|
||||||
if (props.resolveTempUrl) {
|
if (props.resolveTempUrl) {
|
||||||
return props.resolveTempUrl(tempFileId);
|
return await props.resolveTempUrl(tempFileId);
|
||||||
}
|
}
|
||||||
return AttachmentAPI.getTempReadURL(tempFileId);
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function convertContentForEditor(value?: string) {
|
async function convertContentForEditor(value?: string) {
|
||||||
@@ -158,7 +140,10 @@ async function convertContentForEditor(value?: string) {
|
|||||||
if (tempFileId) {
|
if (tempFileId) {
|
||||||
media.setAttribute(TEMP_FILE_ID_ATTR, tempFileId);
|
media.setAttribute(TEMP_FILE_ID_ATTR, tempFileId);
|
||||||
media.setAttribute(ATTACHMENT_ID_ATTR, attachmentId || tempFileId);
|
media.setAttribute(ATTACHMENT_ID_ATTR, attachmentId || tempFileId);
|
||||||
media.setAttribute("src", await resolveTempPreviewUrl(tempFileId));
|
const previewUrl = await resolveTempPreviewUrl(tempFileId);
|
||||||
|
if (previewUrl) {
|
||||||
|
media.setAttribute("src", previewUrl);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!attachmentId) {
|
if (!attachmentId) {
|
||||||
@@ -189,7 +174,7 @@ function buildStoredContent(content: string, currentContentType: ArticleContentT
|
|||||||
|
|
||||||
function buildArticleAttachmentList(content: string, currentContentType: ArticleContentType) {
|
function buildArticleAttachmentList(content: string, currentContentType: ArticleContentType) {
|
||||||
if (currentContentType === "TIP_TAP") {
|
if (currentContentType === "TIP_TAP") {
|
||||||
return buildAttachmentListFromContent(content);
|
return buildAttachmentListFromContent(content, props.modelValue?.attachmentList);
|
||||||
}
|
}
|
||||||
return props.modelValue?.attachmentList || [];
|
return props.modelValue?.attachmentList || [];
|
||||||
}
|
}
|
||||||
@@ -263,7 +248,7 @@ watch(() => props.modelValue?.attachmentList, async (value) => {
|
|||||||
if (sameAttachmentList(lastOutgoingAttachmentList.value, value)) {
|
if (sameAttachmentList(lastOutgoingAttachmentList.value, value)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (sameAttachmentList(buildAttachmentListFromContent(editorContent.value), value)) {
|
if (sameAttachmentList(buildAttachmentListFromContent(editorContent.value, value), value)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const nextContent = await convertContentForEditor(props.modelValue?.content);
|
const nextContent = await convertContentForEditor(props.modelValue?.content);
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { UploadResult as EditorUploadResult } from "timi-web/editor";
|
||||||
|
|
||||||
|
/** 文章编辑器支持的正文类型。 */
|
||||||
|
export type ArticleContentType = "TEXT" | "MARKDOWN" | "TIP_TAP";
|
||||||
|
|
||||||
|
/** 文章编辑器需要的附件最小结构,允许业务对象携带其他字段。 */
|
||||||
|
export type Attachment = {
|
||||||
|
[key: string]: unknown;
|
||||||
|
id?: string;
|
||||||
|
tempFileId?: string;
|
||||||
|
name?: string;
|
||||||
|
url?: string;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
accessKey?: {
|
||||||
|
value?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 文章编辑器需要的文章最小结构,业务对象的其他字段会原样保留。 */
|
||||||
|
export type Article = {
|
||||||
|
[key: string]: unknown;
|
||||||
|
contentType?: string;
|
||||||
|
content?: string;
|
||||||
|
attachmentList?: Attachment[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 媒体上传结果。 */
|
||||||
|
export type UploadResult = EditorUploadResult;
|
||||||
@@ -13,17 +13,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Article, Attachment } from "timi-web/editor";
|
import type { Article, Attachment } from "../article-editor/types";
|
||||||
import { AttachmentAPI } from "timi-web/editor";
|
|
||||||
import { MarkdownView } from "timi-web";
|
import { MarkdownView } from "timi-web";
|
||||||
import { TiptapView } from "../tiptap-view";
|
import { TiptapView } from "../tiptap-view";
|
||||||
import {
|
import {
|
||||||
ATTACHMENT_ID_ATTR,
|
|
||||||
getAttachmentIdFromSrc,
|
getAttachmentIdFromSrc,
|
||||||
normalizeArticleContentType,
|
normalizeArticleContentType,
|
||||||
normalizeContent,
|
|
||||||
normalizeRawArticleContent
|
normalizeRawArticleContent
|
||||||
} from "timi-web/editor";
|
} from "../article-editor/content";
|
||||||
|
import { ATTACHMENT_ID_ATTR, normalizeContent } from "timi-web/editor";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "ArticleView"
|
name: "ArticleView"
|
||||||
@@ -69,11 +67,13 @@ async function buildAttachmentPreviewUrl(attachmentId: string) {
|
|||||||
return customUrl;
|
return customUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const previewUrl = `${attachment?.metadata?.previewUrl || ""}`;
|
const previewUrl = typeof attachment?.metadata?.previewUrl === "string"
|
||||||
|
? attachment.metadata.previewUrl
|
||||||
|
: "";
|
||||||
if (previewUrl) {
|
if (previewUrl) {
|
||||||
return previewUrl;
|
return previewUrl;
|
||||||
}
|
}
|
||||||
return attachmentId ? AttachmentAPI.getReadURL(attachmentId) : "";
|
return attachment?.url || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildRenderContent(content?: string) {
|
async function buildRenderContent(content?: string) {
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import view from "./index.vue";
|
||||||
|
import { Toolkit } from "timi-web";
|
||||||
|
|
||||||
|
export const BottomPopup = Toolkit.withInstall(view);
|
||||||
|
export default BottomPopup;
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
<template>
|
||||||
|
<t-popup v-model="visible" placement="bottom">
|
||||||
|
<div class="bottom-popup" :style="{ height: props.height }">
|
||||||
|
<div v-if="hasHeader" class="header">
|
||||||
|
<div class="button left">
|
||||||
|
<slot name="leftButton" :on-click="emitLeftButtonClick">
|
||||||
|
<t-button v-if="leftButtonText" variant="text" size="small" @click="emitLeftButtonClick">
|
||||||
|
<span v-text="leftButtonText" />
|
||||||
|
</t-button>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<div class="title">
|
||||||
|
<slot name="title">
|
||||||
|
<span v-text="title" />
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<div class="button right">
|
||||||
|
<slot name="rightButton" :on-click="emitRightButtonClick">
|
||||||
|
<t-button v-if="rightButtonText" theme="primary" variant="text" size="small" @click="emitRightButtonClick">
|
||||||
|
<span v-text="rightButtonText" />
|
||||||
|
</t-button>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="body">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t-popup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
defineOptions({
|
||||||
|
name: "BottomPopup"
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface BottomPopupProps {
|
||||||
|
modelValue?: boolean;
|
||||||
|
leftButtonText?: string;
|
||||||
|
rightButtonText?: string;
|
||||||
|
height?: string | number;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BottomPopupSlots {
|
||||||
|
default?: () => unknown;
|
||||||
|
title?: () => unknown;
|
||||||
|
leftButton?: (props: { onClick: () => void }) => unknown;
|
||||||
|
rightButton?: (props: { onClick: () => void }) => unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<BottomPopupProps>(), {
|
||||||
|
modelValue: false,
|
||||||
|
leftButtonText: "取消",
|
||||||
|
rightButtonText: "确定",
|
||||||
|
height: "auto",
|
||||||
|
title: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"update:modelValue": [value: boolean];
|
||||||
|
leftButtonClick: [];
|
||||||
|
rightButtonClick: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const slots = useSlots() as BottomPopupSlots;
|
||||||
|
const visible = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: value => emit("update:modelValue", value)
|
||||||
|
});
|
||||||
|
const hasHeader = computed<boolean>(() => Boolean(
|
||||||
|
props.title
|
||||||
|
|| props.leftButtonText
|
||||||
|
|| props.rightButtonText
|
||||||
|
|| slots.title
|
||||||
|
|| slots.leftButton
|
||||||
|
|| slots.rightButton
|
||||||
|
));
|
||||||
|
|
||||||
|
function emitLeftButtonClick(): void {
|
||||||
|
emit("leftButtonClick");
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitRightButtonClick(): void {
|
||||||
|
emit("rightButtonClick");
|
||||||
|
}
|
||||||
|
|
||||||
|
defineSlots<BottomPopupSlots>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.bottom-popup {
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-top: 1rem;
|
||||||
|
background: var(--td-bg-color-container, #fff);
|
||||||
|
border-radius: 1.25rem 1.25rem 0 0;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
flex: none;
|
||||||
|
padding: 0 1rem;
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
grid-template-columns: 4rem 1fr 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
&.right {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
flex: 1;
|
||||||
|
padding: .75rem 0 env(safe-area-inset-bottom, 0);
|
||||||
|
overflow: auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+17
-2
@@ -8,6 +8,7 @@ import ArticleEditor from "./article-editor";
|
|||||||
import ArticleView from "./article-view";
|
import ArticleView from "./article-view";
|
||||||
import NotifyEditor from "./notify-editor";
|
import NotifyEditor from "./notify-editor";
|
||||||
import NotifyView from "./notify-view";
|
import NotifyView from "./notify-view";
|
||||||
|
import BottomPopup from "./bottom-popup";
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
RootLayout,
|
RootLayout,
|
||||||
@@ -18,7 +19,8 @@ export default [
|
|||||||
ArticleEditor,
|
ArticleEditor,
|
||||||
ArticleView,
|
ArticleView,
|
||||||
NotifyEditor,
|
NotifyEditor,
|
||||||
NotifyView
|
NotifyView,
|
||||||
|
BottomPopup
|
||||||
];
|
];
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -30,7 +32,8 @@ export {
|
|||||||
ArticleEditor,
|
ArticleEditor,
|
||||||
ArticleView,
|
ArticleView,
|
||||||
NotifyEditor,
|
NotifyEditor,
|
||||||
NotifyView
|
NotifyView,
|
||||||
|
BottomPopup
|
||||||
};
|
};
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
@@ -48,3 +51,15 @@ export type {
|
|||||||
export type {
|
export type {
|
||||||
NotifyEditorExpose
|
NotifyEditorExpose
|
||||||
} from "./notify-editor";
|
} from "./notify-editor";
|
||||||
|
|
||||||
|
export type {
|
||||||
|
Article,
|
||||||
|
ArticleContentType,
|
||||||
|
Attachment,
|
||||||
|
UploadResult
|
||||||
|
} from "./article-editor";
|
||||||
|
|
||||||
|
export type {
|
||||||
|
NotifyContentType,
|
||||||
|
NotifyDetail
|
||||||
|
} from "./notify-editor";
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import type { NotifyContentType } from "./types";
|
||||||
|
|
||||||
|
/** 将未知通知正文类型归一化为组件支持的类型。 */
|
||||||
|
export function normalizeNotifyContentType(contentType?: string): NotifyContentType {
|
||||||
|
return contentType === "TEXT" ? "TEXT" : "TIP_TAP";
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import view from "./index.vue";
|
import view from "./index.vue";
|
||||||
import { Toolkit } from "timi-web";
|
import { Toolkit } from "timi-web";
|
||||||
import type { NotifyDetail } from "timi-web/editor";
|
import type { NotifyDetail } from "./types";
|
||||||
|
|
||||||
|
export type { NotifyContentType, NotifyDetail } from "./types";
|
||||||
|
|
||||||
export type NotifyEditorExpose = {
|
export type NotifyEditorExpose = {
|
||||||
submit: () => Promise<NotifyDetail>;
|
submit: () => Promise<NotifyDetail>;
|
||||||
|
|||||||
@@ -13,10 +13,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Article, ArticleContentType, Attachment, NotifyDetail, UploadResult } from "timi-web/editor";
|
import type { Article, ArticleContentType, Attachment, UploadResult } from "../article-editor/types";
|
||||||
|
import type { NotifyDetail } from "./types";
|
||||||
import type { ArticleEditorExpose } from "../article-editor";
|
import type { ArticleEditorExpose } from "../article-editor";
|
||||||
import { ArticleEditor } from "../article-editor";
|
import { ArticleEditor } from "../article-editor";
|
||||||
import { normalizeNotifyContentType } from "timi-web/editor";
|
import { normalizeNotifyContentType } from "./content";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "NotifyEditor"
|
name: "NotifyEditor"
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { Article, Attachment } from "../article-editor/types";
|
||||||
|
|
||||||
|
/** 通知内容支持的正文类型。 */
|
||||||
|
export type NotifyContentType = "TEXT" | "TIP_TAP";
|
||||||
|
|
||||||
|
/** 通知编辑器需要的通知最小结构。 */
|
||||||
|
export type NotifyDetail = {
|
||||||
|
[key: string]: unknown;
|
||||||
|
content?: string;
|
||||||
|
contentType?: string;
|
||||||
|
attachmentList?: Attachment[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 通知内部复用文章编辑器的对象结构。 */
|
||||||
|
export type NotifyArticle = Article;
|
||||||
@@ -7,9 +7,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Article, ArticleContentType, Attachment, NotifyDetail } from "timi-web/editor";
|
import type { Article, ArticleContentType, Attachment } from "../article-editor/types";
|
||||||
|
import type { NotifyDetail } from "../notify-editor/types";
|
||||||
import { ArticleView } from "../article-view";
|
import { ArticleView } from "../article-view";
|
||||||
import { normalizeNotifyContentType } from "timi-web/editor";
|
import { normalizeNotifyContentType } from "../notify-editor/content";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "NotifyView"
|
name: "NotifyView"
|
||||||
|
|||||||
@@ -1,30 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<t-button v-popup="'代码块'" variant="outline" :theme="active ? 'primary' : 'default'" @click="openDialog">
|
<t-button v-popup="'代码块'" :variant="active ? 'base' : 'text'" :theme="active ? 'light' : 'default'" @click="openDialog">
|
||||||
<t-icon name="code" />
|
<t-icon name="code" />
|
||||||
</t-button>
|
</t-button>
|
||||||
<t-popup
|
<bottom-popup
|
||||||
v-model="visible"
|
v-model="visible"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
|
title="插入代码块"
|
||||||
|
left-button-text="取消"
|
||||||
|
right-button-text="插入"
|
||||||
|
@left-button-click="visible = false"
|
||||||
|
@right-button-click="onConfirm"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
>
|
>
|
||||||
<div class="form-popup">
|
<div class="form-popup">
|
||||||
<div class="popup-head">
|
|
||||||
<span class="title">插入代码块</span>
|
|
||||||
<t-button shape="square" size="small" variant="text" title="关闭" @click="visible = false">
|
|
||||||
<t-icon name="close" />
|
|
||||||
</t-button>
|
|
||||||
</div>
|
|
||||||
<t-input v-model="codeLanguage" placeholder="代码语言,可留空" />
|
<t-input v-model="codeLanguage" placeholder="代码语言,可留空" />
|
||||||
<div class="popup-actions">
|
|
||||||
<t-button variant="outline" block @click="visible = false">取消</t-button>
|
|
||||||
<t-button theme="primary" block @click="onConfirm">插入</t-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</bottom-popup>
|
||||||
</t-popup>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { TiptapEditorInstance } from "timi-web/editor";
|
import type { TiptapEditorInstance } from "timi-web/editor";
|
||||||
|
import BottomPopup from "../bottom-popup/index.vue";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "TiptapCodeBlockButton"
|
name: "TiptapCodeBlockButton"
|
||||||
@@ -69,27 +65,5 @@ function onConfirm() {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background: var(--td-bg-color-container);
|
background: var(--td-bg-color-container);
|
||||||
|
|
||||||
.popup-head {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--td-text-color-primary, #000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-actions {
|
|
||||||
gap: .75rem;
|
|
||||||
display: flex;
|
|
||||||
margin-top: 1rem;
|
|
||||||
|
|
||||||
:deep(.t-button) {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<t-button v-popup="'插入内嵌网页'" variant="outline" :theme="active ? 'primary' : 'default'" @click="openDialog">
|
<t-button v-popup="'插入内嵌网页'" :variant="active ? 'base' : 'text'" :theme="active ? 'light' : 'default'" @click="openDialog">
|
||||||
<t-icon name="web" />
|
<t-icon name="web" />
|
||||||
</t-button>
|
</t-button>
|
||||||
<t-popup
|
<bottom-popup
|
||||||
v-model="visible"
|
v-model="visible"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
|
title="插入内嵌网页"
|
||||||
|
left-button-text="取消"
|
||||||
|
right-button-text="插入"
|
||||||
|
@left-button-click="visible = false"
|
||||||
|
@right-button-click="onConfirm"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
>
|
>
|
||||||
<div class="form-popup">
|
<div class="form-popup">
|
||||||
<div class="popup-head">
|
|
||||||
<span class="title">插入内嵌网页</span>
|
|
||||||
<t-button shape="square" size="small" variant="text" title="关闭" @click="visible = false">
|
|
||||||
<t-icon name="close" />
|
|
||||||
</t-button>
|
|
||||||
</div>
|
|
||||||
<t-form class="form-content" label-align="left" label-width="auto">
|
<t-form class="form-content" label-align="left" label-width="auto">
|
||||||
<t-form-item label="网页地址">
|
<t-form-item label="网页地址">
|
||||||
<t-input v-model="iframeForm.src" placeholder="请输入网页地址" />
|
<t-input v-model="iframeForm.src" placeholder="请输入网页地址" />
|
||||||
@@ -28,17 +27,14 @@
|
|||||||
<t-input v-model="iframeForm.height" placeholder="请输入高度,默认 28rem" />
|
<t-input v-model="iframeForm.height" placeholder="请输入高度,默认 28rem" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
</t-form>
|
</t-form>
|
||||||
<div class="popup-actions">
|
|
||||||
<t-button variant="outline" block @click="visible = false">取消</t-button>
|
|
||||||
<t-button theme="primary" block @click="onConfirm">插入</t-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</bottom-popup>
|
||||||
</t-popup>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { showPlatformMessage } from "../message";
|
import { showPlatformMessage } from "../message";
|
||||||
import type { TiptapEditorInstance } from "timi-web/editor";
|
import type { TiptapEditorInstance } from "timi-web/editor";
|
||||||
|
import BottomPopup from "../bottom-popup/index.vue";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "TiptapIframeButton"
|
name: "TiptapIframeButton"
|
||||||
@@ -117,33 +113,11 @@ async function onConfirm() {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background: var(--td-bg-color-container);
|
background: var(--td-bg-color-container);
|
||||||
|
|
||||||
.popup-head {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--td-text-color-primary, #000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-content {
|
.form-content {
|
||||||
:deep(.t-form__item) {
|
:deep(.t-form__item) {
|
||||||
margin-bottom: .875rem;
|
margin-bottom: .875rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-actions {
|
|
||||||
gap: .75rem;
|
|
||||||
display: flex;
|
|
||||||
margin-top: 1rem;
|
|
||||||
|
|
||||||
:deep(.t-button) {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,34 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<t-button v-popup="'插入链接'" variant="outline" :theme="active ? 'primary' : 'default'" @click="openDialog">
|
<t-button v-popup="'插入链接'" :variant="active ? 'base' : 'text'" :theme="active ? 'light' : 'default'" @click="openDialog">
|
||||||
<t-icon name="link-1" />
|
<t-icon name="link-1" />
|
||||||
</t-button>
|
</t-button>
|
||||||
<t-popup
|
<bottom-popup
|
||||||
v-model="visible"
|
v-model="visible"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
|
title="插入链接"
|
||||||
|
left-button-text="取消"
|
||||||
|
right-button-text="插入"
|
||||||
|
@left-button-click="visible = false"
|
||||||
|
@right-button-click="onConfirm"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
>
|
>
|
||||||
<div class="form-popup">
|
<div class="form-popup">
|
||||||
<div class="popup-head">
|
|
||||||
<span class="title">插入链接</span>
|
|
||||||
<t-button shape="square" size="small" variant="text" title="关闭" @click="visible = false">
|
|
||||||
<t-icon name="close" />
|
|
||||||
</t-button>
|
|
||||||
</div>
|
|
||||||
<div class="link-form">
|
<div class="link-form">
|
||||||
<t-input v-model="linkForm.href" placeholder="请输入链接地址" />
|
<t-input v-model="linkForm.href" placeholder="请输入链接地址" />
|
||||||
<t-input v-model="linkForm.text" placeholder="请输入显示文本,可留空" />
|
<t-input v-model="linkForm.text" placeholder="请输入显示文本,可留空" />
|
||||||
<t-checkbox v-model="linkForm.syncToHref">显示文本同步为链接</t-checkbox>
|
<t-checkbox v-model="linkForm.syncToHref">显示文本同步为链接</t-checkbox>
|
||||||
</div>
|
</div>
|
||||||
<div class="popup-actions">
|
|
||||||
<t-button variant="outline" block @click="visible = false">取消</t-button>
|
|
||||||
<t-button theme="primary" block @click="onConfirm">插入</t-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</bottom-popup>
|
||||||
</t-popup>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { TiptapEditorInstance } from "timi-web/editor";
|
import type { TiptapEditorInstance } from "timi-web/editor";
|
||||||
|
import BottomPopup from "../bottom-popup/index.vue";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "TiptapLinkButton"
|
name: "TiptapLinkButton"
|
||||||
@@ -105,27 +101,5 @@ function onConfirm() {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background: var(--td-bg-color-container);
|
background: var(--td-bg-color-container);
|
||||||
|
|
||||||
.popup-head {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--td-text-color-primary, #000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-actions {
|
|
||||||
gap: .75rem;
|
|
||||||
display: flex;
|
|
||||||
margin-top: 1rem;
|
|
||||||
|
|
||||||
:deep(.t-button) {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,19 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<t-button variant="outline" :theme="active ? 'primary' : 'default'" :loading="uploading" @click="openDialog">
|
<t-button :variant="active ? 'base' : 'text'" :theme="active ? 'light' : 'default'" :loading="uploading" @click="openDialog">
|
||||||
<t-icon :name="iconName" />
|
<t-icon :name="iconName" />
|
||||||
</t-button>
|
</t-button>
|
||||||
<t-popup
|
<bottom-popup
|
||||||
v-model="visible"
|
v-model="visible"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
|
:title="`插入${typeLabel}`"
|
||||||
|
left-button-text="取消"
|
||||||
|
right-button-text="插入"
|
||||||
|
@left-button-click="visible = false"
|
||||||
|
@right-button-click="onConfirm"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
>
|
>
|
||||||
<div class="form-popup">
|
<template #rightButton="{ onClick }">
|
||||||
<div class="popup-head">
|
<t-button
|
||||||
<span class="title" v-text="`插入${typeLabel}`" />
|
theme="primary"
|
||||||
<t-button shape="square" size="small" variant="text" title="关闭" @click="visible = false">
|
variant="text"
|
||||||
<t-icon name="close" />
|
size="small"
|
||||||
|
:loading="uploading"
|
||||||
|
:disabled="uploading"
|
||||||
|
@click="onClick"
|
||||||
|
>
|
||||||
|
插入
|
||||||
</t-button>
|
</t-button>
|
||||||
</div>
|
</template>
|
||||||
|
<div class="form-popup">
|
||||||
<t-form class="form-content" label-align="left" label-width="auto">
|
<t-form class="form-content" label-align="left" label-width="auto">
|
||||||
<t-form-item label="文件">
|
<t-form-item label="文件">
|
||||||
<t-button size="small" variant="outline" @click="pickFile">
|
<t-button size="small" variant="outline" @click="pickFile">
|
||||||
@@ -31,18 +42,15 @@
|
|||||||
<t-input v-model="mediaForm.height" placeholder="请输入高度,默认 auto" />
|
<t-input v-model="mediaForm.height" placeholder="请输入高度,默认 auto" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
</t-form>
|
</t-form>
|
||||||
<div class="popup-actions">
|
|
||||||
<t-button variant="outline" block :disabled="uploading" @click="visible = false">取消</t-button>
|
|
||||||
<t-button theme="primary" block :loading="uploading" @click="onConfirm">插入</t-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</bottom-popup>
|
||||||
</t-popup>
|
|
||||||
<input ref="fileInputRef" class="file-input" type="file" :accept="accept" @change="onFileChange" />
|
<input ref="fileInputRef" class="file-input" type="file" :accept="accept" @change="onFileChange" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { showPlatformMessage } from "../message";
|
import { showPlatformMessage } from "../message";
|
||||||
import type { TiptapEditorInstance, UploadResult } from "timi-web/editor";
|
import type { TiptapEditorInstance, UploadResult } from "timi-web/editor";
|
||||||
|
import BottomPopup from "../bottom-popup/index.vue";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "TiptapMediaButton"
|
name: "TiptapMediaButton"
|
||||||
@@ -180,34 +188,12 @@ async function onConfirm() {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background: var(--td-bg-color-container);
|
background: var(--td-bg-color-container);
|
||||||
|
|
||||||
.popup-head {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--td-text-color-primary, #000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-content {
|
.form-content {
|
||||||
:deep(.t-form__item) {
|
:deep(.t-form__item) {
|
||||||
margin-bottom: .875rem;
|
margin-bottom: .875rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-actions {
|
|
||||||
gap: .75rem;
|
|
||||||
display: flex;
|
|
||||||
margin-top: 1rem;
|
|
||||||
|
|
||||||
:deep(.t-button) {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-input {
|
.file-input {
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<t-button v-popup="'插入表格'" variant="outline" :theme="active ? 'primary' : 'default'" @click="openPopup">
|
<t-button v-popup="'插入表格'" :variant="active ? 'base' : 'text'" :theme="active ? 'light' : 'default'" @click="openPopup">
|
||||||
<t-icon name="table" />
|
<t-icon name="table" />
|
||||||
</t-button>
|
</t-button>
|
||||||
<t-popup v-model="visible" placement="bottom" destroy-on-close>
|
<bottom-popup
|
||||||
|
v-model="visible"
|
||||||
|
placement="bottom"
|
||||||
|
title="插入表格"
|
||||||
|
left-button-text="取消"
|
||||||
|
right-button-text="确定"
|
||||||
|
@left-button-click="visible = false"
|
||||||
|
@right-button-click="visible = false"
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
<div class="table-picker" @mouseleave="onReset">
|
<div class="table-picker" @mouseleave="onReset">
|
||||||
<div class="tip" v-text="`${hoverRow} × ${hoverCol}`" />
|
<div class="tip" v-text="`${hoverRow} × ${hoverCol}`" />
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
@@ -17,12 +26,13 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</t-popup>
|
</bottom-popup>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { TABLE_PICKER_SIZE } from "timi-web/editor";
|
import { TABLE_PICKER_SIZE } from "timi-web/editor";
|
||||||
import type { TiptapEditorInstance } from "timi-web/editor";
|
import type { TiptapEditorInstance } from "timi-web/editor";
|
||||||
|
import BottomPopup from "../bottom-popup/index.vue";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "TiptapTableButton"
|
name: "TiptapTableButton"
|
||||||
|
|||||||
@@ -2,21 +2,21 @@
|
|||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<div class="toolbar-head">
|
<div class="toolbar-head">
|
||||||
<div class="history" :class="{ 'toolbar-disabled': sourceCodeMode }">
|
<div class="history" :class="{ 'toolbar-disabled': sourceCodeMode }">
|
||||||
<t-button v-popup="'撤销'" variant="outline" :disabled="!canExec('undo')" @click="run('undo')">
|
<t-button v-popup="'撤销'" variant="text" :disabled="!canExec('undo')" @click="run('undo')">
|
||||||
<t-icon name="rollback" />
|
<t-icon name="rollback" />
|
||||||
</t-button>
|
</t-button>
|
||||||
<t-button v-popup="'重做'" variant="outline" :disabled="!canExec('redo')" @click="run('redo')">
|
<t-button v-popup="'重做'" variant="text" :disabled="!canExec('redo')" @click="run('redo')">
|
||||||
<t-icon name="rollfront" />
|
<t-icon name="rollfront" />
|
||||||
</t-button>
|
</t-button>
|
||||||
<t-button v-popup="'清除格式'" variant="outline" @click="run('clear')">
|
<t-button v-popup="'清除格式'" variant="text" @click="run('clear')">
|
||||||
<t-icon name="clear-formatting" />
|
<t-icon name="clear-formatting" />
|
||||||
</t-button>
|
</t-button>
|
||||||
</div>
|
</div>
|
||||||
<t-button
|
<t-button
|
||||||
v-popup="sourceCodeMode ? '富文本' : '源码'"
|
v-popup="sourceCodeMode ? '富文本' : '源码'"
|
||||||
class="source-btn"
|
class="source-btn"
|
||||||
variant="outline"
|
:variant="sourceCodeMode ? 'base' : 'text'"
|
||||||
:theme="sourceCodeMode ? 'primary' : 'default'"
|
:theme="sourceCodeMode ? 'light' : 'default'"
|
||||||
@click="emit('toggle-source-code')"
|
@click="emit('toggle-source-code')"
|
||||||
>
|
>
|
||||||
<t-icon v-if="sourceCodeMode" name="edit-1" />
|
<t-icon v-if="sourceCodeMode" name="edit-1" />
|
||||||
@@ -25,10 +25,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="scroll" :class="{ 'toolbar-disabled': sourceCodeMode }">
|
<div class="scroll" :class="{ 'toolbar-disabled': sourceCodeMode }">
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<t-button v-popup="'标题'" variant="outline" :theme="isHeadingActive ? 'primary' : 'default'" @click="headingVisible = true">
|
<t-button v-popup="'标题'" :variant="isHeadingActive ? 'base' : 'text'" :theme="isHeadingActive ? 'light' : 'default'" @click="headingVisible = true">
|
||||||
<t-icon name="text" />
|
<t-icon name="text" />
|
||||||
</t-button>
|
</t-button>
|
||||||
<t-popup v-model="headingVisible" placement="bottom" destroy-on-close>
|
<bottom-popup
|
||||||
|
v-model="headingVisible"
|
||||||
|
placement="bottom"
|
||||||
|
title="选择标题"
|
||||||
|
left-button-text="取消"
|
||||||
|
right-button-text="确定"
|
||||||
|
@left-button-click="headingVisible = false"
|
||||||
|
@right-button-click="headingVisible = false"
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
<div class="heading-picker">
|
<div class="heading-picker">
|
||||||
<t-button
|
<t-button
|
||||||
v-for="option in headingMeta"
|
v-for="option in headingMeta"
|
||||||
@@ -40,55 +49,60 @@
|
|||||||
<span :class="['tp-heading-option', option.className]" v-text="option.label" />
|
<span :class="['tp-heading-option', option.className]" v-text="option.label" />
|
||||||
</t-button>
|
</t-button>
|
||||||
</div>
|
</div>
|
||||||
</t-popup>
|
</bottom-popup>
|
||||||
<t-button v-popup="'加粗'" variant="outline" :theme="isActive('bold') ? 'primary' : 'default'" @click="run('bold')">
|
<t-button v-popup="'加粗'" :variant="isActive('bold') ? 'base' : 'text'" :theme="isActive('bold') ? 'light' : 'default'" @click="run('bold')">
|
||||||
<t-icon name="textformat-bold" />
|
<t-icon name="textformat-bold" />
|
||||||
</t-button>
|
</t-button>
|
||||||
<t-button v-popup="'斜体'" variant="outline" :theme="isActive('italic') ? 'primary' : 'default'" @click="run('italic')">
|
<t-button v-popup="'斜体'" :variant="isActive('italic') ? 'base' : 'text'" :theme="isActive('italic') ? 'light' : 'default'" @click="run('italic')">
|
||||||
<t-icon name="textformat-italic" />
|
<t-icon name="textformat-italic" />
|
||||||
</t-button>
|
</t-button>
|
||||||
<t-button v-popup="'下划线'" variant="outline" :theme="isActive('underline') ? 'primary' : 'default'" @click="run('underline')">
|
<t-button v-popup="'下划线'" :variant="isActive('underline') ? 'base' : 'text'" :theme="isActive('underline') ? 'light' : 'default'" @click="run('underline')">
|
||||||
<t-icon name="textformat-underline" />
|
<t-icon name="textformat-underline" />
|
||||||
</t-button>
|
</t-button>
|
||||||
<t-button v-popup="'删除线'" variant="outline" :theme="isActive('strike') ? 'primary' : 'default'" @click="run('strike')">
|
<t-button v-popup="'删除线'" :variant="isActive('strike') ? 'base' : 'text'" :theme="isActive('strike') ? 'light' : 'default'" @click="run('strike')">
|
||||||
<t-icon name="textformat-strikethrough" />
|
<t-icon name="textformat-strikethrough" />
|
||||||
</t-button>
|
</t-button>
|
||||||
<t-button v-popup="'前景色'" variant="outline" @click="pickColor('text')">
|
<t-button v-popup="'前景色'" variant="text" @click="pickColor('text')">
|
||||||
<t-icon name="textformat-color" />
|
<t-icon name="textformat-color" />
|
||||||
</t-button>
|
</t-button>
|
||||||
<t-button v-popup="'背景色'" variant="outline" @click="pickColor('bg')">
|
<t-button v-popup="'背景色'" variant="text" @click="pickColor('bg')">
|
||||||
<t-icon name="fill-color-1" />
|
<t-icon name="fill-color-1" />
|
||||||
</t-button>
|
</t-button>
|
||||||
<t-button v-popup="'有序列表'" variant="outline" :theme="isActive('orderedList') ? 'primary' : 'default'" @click="run('orderedList')">
|
<t-button v-popup="'有序列表'" :variant="isActive('orderedList') ? 'base' : 'text'" :theme="isActive('orderedList') ? 'light' : 'default'" @click="run('orderedList')">
|
||||||
<t-icon name="order-list" />
|
<t-icon name="order-list" />
|
||||||
</t-button>
|
</t-button>
|
||||||
<tiptap-table-button :editor="editor" :active="isActive('table')" />
|
<tiptap-table-button :editor="editor" :active="isActive('table')" />
|
||||||
<t-button v-popup="'无序列表'" variant="outline" :theme="isActive('bulletList') ? 'primary' : 'default'" @click="run('bulletList')">
|
<t-button v-popup="'无序列表'" :variant="isActive('bulletList') ? 'base' : 'text'" :theme="isActive('bulletList') ? 'light' : 'default'" @click="run('bulletList')">
|
||||||
<t-icon name="bulletpoint" />
|
<t-icon name="bulletpoint" />
|
||||||
</t-button>
|
</t-button>
|
||||||
<t-button v-popup="'行内代码'" variant="outline" :theme="isActive('code') ? 'primary' : 'default'" @click="run('code')">
|
<t-button v-popup="'行内代码'" :variant="isActive('code') ? 'base' : 'text'" :theme="isActive('code') ? 'light' : 'default'" @click="run('code')">
|
||||||
<t-icon name="highlighted-block" />
|
<t-icon name="highlighted-block" />
|
||||||
</t-button>
|
</t-button>
|
||||||
<tiptap-code-block-button :editor="editor" :active="isActive('codeBlock')" />
|
<tiptap-code-block-button :editor="editor" :active="isActive('codeBlock')" />
|
||||||
<t-button v-popup="'下标'" variant="outline" :theme="isActive('subscript') ? 'primary' : 'default'" @click="run('subscript')">
|
<t-button v-popup="'下标'" :variant="isActive('subscript') ? 'base' : 'text'" :theme="isActive('subscript') ? 'light' : 'default'" @click="run('subscript')">
|
||||||
<t-icon name="subscript" />
|
<t-icon name="subscript" />
|
||||||
</t-button>
|
</t-button>
|
||||||
<t-button v-popup="'上标'" variant="outline" :theme="isActive('superscript') ? 'primary' : 'default'" @click="run('superscript')">
|
<t-button v-popup="'上标'" :variant="isActive('superscript') ? 'base' : 'text'" :theme="isActive('superscript') ? 'light' : 'default'" @click="run('superscript')">
|
||||||
<t-icon name="superscript" />
|
<t-icon name="superscript" />
|
||||||
</t-button>
|
</t-button>
|
||||||
<t-button v-popup="'左对齐'" variant="outline" :theme="isAlignActive('left') ? 'primary' : 'default'" @click="setAlign('left')">
|
<t-button v-popup="'左对齐'" :variant="isAlignActive('left') ? 'base' : 'text'" :theme="isAlignActive('left') ? 'light' : 'default'" @click="setAlign('left')">
|
||||||
<t-icon name="format-vertical-align-left" />
|
<t-icon name="format-vertical-align-left" />
|
||||||
</t-button>
|
</t-button>
|
||||||
<t-button v-popup="'居中对齐'" variant="outline" :theme="isAlignActive('center') ? 'primary' : 'default'" @click="setAlign('center')">
|
<t-button v-popup="'居中对齐'" :variant="isAlignActive('center') ? 'base' : 'text'" :theme="isAlignActive('center') ? 'light' : 'default'" @click="setAlign('center')">
|
||||||
<t-icon name="format-vertical-align-center" />
|
<t-icon name="format-vertical-align-center" />
|
||||||
</t-button>
|
</t-button>
|
||||||
<t-button v-popup="'右对齐'" variant="outline" :theme="isAlignActive('right') ? 'primary' : 'default'" @click="setAlign('right')">
|
<t-button v-popup="'右对齐'" :variant="isAlignActive('right') ? 'base' : 'text'" :theme="isAlignActive('right') ? 'light' : 'default'" @click="setAlign('right')">
|
||||||
<t-icon name="format-vertical-align-right" />
|
<t-icon name="format-vertical-align-right" />
|
||||||
</t-button>
|
</t-button>
|
||||||
<t-button v-popup="'两端对齐'" variant="outline" :theme="isAlignActive('justify') ? 'primary' : 'default'" @click="setAlign('justify')">
|
<t-button
|
||||||
|
v-popup="'两端对齐'"
|
||||||
|
:variant="isAlignActive('justify') ? 'base' : 'text'"
|
||||||
|
:theme="isAlignActive('justify') ? 'light' : 'default'"
|
||||||
|
@click="setAlign('justify')"
|
||||||
|
>
|
||||||
<t-icon name="expand-horizontal" />
|
<t-icon name="expand-horizontal" />
|
||||||
</t-button>
|
</t-button>
|
||||||
<t-button v-popup="'分割线'" variant="outline" @click="run('horizontalRule')">
|
<t-button v-popup="'分割线'" variant="text" @click="run('horizontalRule')">
|
||||||
<t-icon name="divider-1" />
|
<t-icon name="divider-1" />
|
||||||
</t-button>
|
</t-button>
|
||||||
<tiptap-link-button :editor="editor" :active="isActive('link')" />
|
<tiptap-link-button :editor="editor" :active="isActive('link')" />
|
||||||
@@ -97,12 +111,46 @@
|
|||||||
<tiptap-iframe-button :editor="editor" :active="isActive('iframe')" />
|
<tiptap-iframe-button :editor="editor" :active="isActive('iframe')" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input ref="textColorRef" class="hidden" type="color" :value="textColor" @input="onTextColorChange" />
|
<bottom-popup
|
||||||
<input ref="bgColorRef" class="hidden" type="color" :value="bgColor" @input="onBgColorChange" />
|
v-model="colorPickerVisible"
|
||||||
|
placement="bottom"
|
||||||
|
:title="colorPickerType === 'text' ? '前景色' : '背景色'"
|
||||||
|
left-button-text="取消"
|
||||||
|
right-button-text="确定"
|
||||||
|
@left-button-click="colorPickerVisible = false"
|
||||||
|
@right-button-click="confirmColor"
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<div class="color-picker-popup">
|
||||||
|
<template v-if="colorPickerType === 'text'">
|
||||||
|
<t-color-picker
|
||||||
|
v-model="textColorDraft"
|
||||||
|
type="multiple"
|
||||||
|
format="HEX"
|
||||||
|
:fixed="true"
|
||||||
|
:swatch-colors="colorSwatchColors"
|
||||||
|
@change="onTextColorChange"
|
||||||
|
@palette-bar-change="onTextPaletteBarChange"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<t-color-picker
|
||||||
|
v-model="bgColorDraft"
|
||||||
|
type="multiple"
|
||||||
|
format="HEX"
|
||||||
|
:fixed="true"
|
||||||
|
:swatch-colors="colorSwatchColors"
|
||||||
|
@change="onBgColorChange"
|
||||||
|
@palette-bar-change="onBgPaletteBarChange"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</bottom-popup>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { ColorObject } from "tdesign-mobile-vue";
|
||||||
import type { AlignType, UploadResult, TiptapEditorInstance } from "timi-web/editor";
|
import type { AlignType, UploadResult, TiptapEditorInstance } from "timi-web/editor";
|
||||||
import { headingMeta } from "timi-web/editor";
|
import { headingMeta } from "timi-web/editor";
|
||||||
import TiptapCodeBlockButton from "./TiptapCodeBlockButton.vue";
|
import TiptapCodeBlockButton from "./TiptapCodeBlockButton.vue";
|
||||||
@@ -110,6 +158,7 @@ import TiptapIframeButton from "./TiptapIframeButton.vue";
|
|||||||
import TiptapLinkButton from "./TiptapLinkButton.vue";
|
import TiptapLinkButton from "./TiptapLinkButton.vue";
|
||||||
import TiptapMediaButton from "./TiptapMediaButton.vue";
|
import TiptapMediaButton from "./TiptapMediaButton.vue";
|
||||||
import TiptapTableButton from "./TiptapTableButton.vue";
|
import TiptapTableButton from "./TiptapTableButton.vue";
|
||||||
|
import BottomPopup from "../bottom-popup/index.vue";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "TiptapToolbar"
|
name: "TiptapToolbar"
|
||||||
@@ -143,11 +192,27 @@ type RunType =
|
|||||||
| "horizontalRule"
|
| "horizontalRule"
|
||||||
| "clear";
|
| "clear";
|
||||||
|
|
||||||
const textColorRef = ref<HTMLInputElement>();
|
const textColorDraft = ref("#000000");
|
||||||
const bgColorRef = ref<HTMLInputElement>();
|
const bgColorDraft = ref("#ffff00");
|
||||||
const textColor = ref("#000000");
|
|
||||||
const bgColor = ref("#ffff00");
|
|
||||||
const headingVisible = ref(false);
|
const headingVisible = ref(false);
|
||||||
|
const colorPickerVisible = ref(false);
|
||||||
|
const colorPickerType = ref<"text" | "bg">("text");
|
||||||
|
const colorSwatchColors = [
|
||||||
|
"#F5222D",
|
||||||
|
"#FF7875",
|
||||||
|
"#FA541C",
|
||||||
|
"#FA8C16",
|
||||||
|
"#FADB14",
|
||||||
|
"#A0D911",
|
||||||
|
"#52C41A",
|
||||||
|
"#13C2C2",
|
||||||
|
"#1890FF",
|
||||||
|
"#2F54EB",
|
||||||
|
"#722ED1",
|
||||||
|
"#EB2F96",
|
||||||
|
"#8C8C8C",
|
||||||
|
"#D9D9D9"
|
||||||
|
];
|
||||||
|
|
||||||
const currentHeadingValue = computed(() => {
|
const currentHeadingValue = computed(() => {
|
||||||
if (!props.editor) {
|
if (!props.editor) {
|
||||||
@@ -264,29 +329,46 @@ function onHeadingSelect(option: { value?: string | number | Record<string, unkn
|
|||||||
}
|
}
|
||||||
|
|
||||||
function pickColor(type: "text" | "bg") {
|
function pickColor(type: "text" | "bg") {
|
||||||
|
const textStyleAttributes = props.editor?.getAttributes("textStyle") ?? {};
|
||||||
if (type === "text") {
|
if (type === "text") {
|
||||||
textColorRef.value?.click();
|
const color = textStyleAttributes.color;
|
||||||
return;
|
textColorDraft.value = typeof color === "string" && color ? color : "#000000";
|
||||||
|
} else {
|
||||||
|
const backgroundColor = textStyleAttributes.backgroundColor;
|
||||||
|
bgColorDraft.value = typeof backgroundColor === "string" && backgroundColor ? backgroundColor : "#ffff00";
|
||||||
}
|
}
|
||||||
bgColorRef.value?.click();
|
colorPickerType.value = type;
|
||||||
|
colorPickerVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTextColorChange(event: Event) {
|
function onTextColorChange(value: string) {
|
||||||
if (!props.editor) {
|
textColorDraft.value = value;
|
||||||
return;
|
|
||||||
}
|
|
||||||
const target = event.target as HTMLInputElement;
|
|
||||||
textColor.value = target.value;
|
|
||||||
props.editor.chain().focus().setColor(target.value).run();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onBgColorChange(event: Event) {
|
function onBgColorChange(value: string) {
|
||||||
|
bgColorDraft.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTextPaletteBarChange(context: { color: ColorObject }) {
|
||||||
|
textColorDraft.value = context.color.hex;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBgPaletteBarChange(context: { color: ColorObject }) {
|
||||||
|
bgColorDraft.value = context.color.hex;
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmColor() {
|
||||||
if (!props.editor) {
|
if (!props.editor) {
|
||||||
|
colorPickerVisible.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const target = event.target as HTMLInputElement;
|
const chain = props.editor.chain().focus();
|
||||||
bgColor.value = target.value;
|
if (colorPickerType.value === "text") {
|
||||||
props.editor.chain().focus().setMark("textStyle", { backgroundColor: target.value }).run();
|
chain.setColor(textColorDraft.value).run();
|
||||||
|
} else {
|
||||||
|
chain.setMark("textStyle", { backgroundColor: bgColorDraft.value }).run();
|
||||||
|
}
|
||||||
|
colorPickerVisible.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -372,6 +454,15 @@ function onBgColorChange(event: Event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.t-button.t-button--text.t-button--hover::after) {
|
||||||
|
background: var(--td-brand-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.t-button--text.t-button--disabled) {
|
||||||
|
background: none;
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
|
||||||
.toolbar-disabled {
|
.toolbar-disabled {
|
||||||
opacity: .55;
|
opacity: .55;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
@@ -402,4 +493,8 @@ function onBgColorChange(event: Event) {
|
|||||||
width: min(22rem, calc(100vw - 2rem));
|
width: min(22rem, calc(100vw - 2rem));
|
||||||
padding: .5rem .75rem calc(.5rem + env(safe-area-inset-bottom));
|
padding: .5rem .75rem calc(.5rem + env(safe-area-inset-bottom));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.color-picker-popup {
|
||||||
|
background: var(--td-bg-color-container);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+1
-2
@@ -1,5 +1,4 @@
|
|||||||
import "./assets/style/tencent-cloud.less";
|
import "./assets/style/tdesign-custom.less";
|
||||||
import "./assets/style/tencent-cloud-custom.less";
|
|
||||||
import "./components/tiptap-content.less";
|
import "./components/tiptap-content.less";
|
||||||
import "timi-web/style.css";
|
import "timi-web/style.css";
|
||||||
import "../../timi-web/src/components/prism-code-block.less";
|
import "../../timi-web/src/components/prism-code-block.less";
|
||||||
|
|||||||
+4
-1
@@ -58,7 +58,10 @@ export default defineConfig({
|
|||||||
include: [/\.vue$/, /\.md$/]
|
include: [/\.vue$/, /\.md$/]
|
||||||
}),
|
}),
|
||||||
VueSetupExtend(),
|
VueSetupExtend(),
|
||||||
dts(),
|
dts({
|
||||||
|
pathsToAliases: false,
|
||||||
|
aliasesExclude: [/^timi-web\/editor$/]
|
||||||
|
}),
|
||||||
AutoImport({
|
AutoImport({
|
||||||
imports: [
|
imports: [
|
||||||
"vue",
|
"vue",
|
||||||
|
|||||||
Reference in New Issue
Block a user