粘贴拖拽图片体验优化

This commit is contained in:
xiaoqi.cxq 2022-10-22 15:43:07 +08:00
parent 058fcaa147
commit e7450df251
7 changed files with 217 additions and 189 deletions

View File

@ -14,6 +14,8 @@ import CommentList from './gutters/CommentList';
import EditorNewDiscussionButton from './gutters/EditorNewDiscussionButton';
import store from '../store';
import editorSvc from '../services/editorSvc';
import imageSvc from '../services/imageSvc';
import utils from '../services/utils';
export default {
components: {
@ -32,7 +34,7 @@ export default {
]),
},
methods: {
setImgAndDoClick(items) {
async setImgAndDoClick(items) {
let file = null;
if (!items || items.length === 0) {
return;
@ -46,8 +48,23 @@ export default {
if (!file) {
return;
}
store.dispatch('img/setImg', file);
editorSvc.pagedownEditor.uiManager.doClick('image');
const imgId = utils.uid();
store.dispatch('img/setCurrImgId', imgId);
editorSvc.pagedownEditor.uiManager.doClick('imageUploading');
try {
const { url, error } = await imageSvc.updateImg(file);
//
if (error) {
editorSvc.clEditor.replaceAll(`[图片上传中...(image-${imgId})]`, `[图片上传失败...(image-${imgId})]`);
store.dispatch('notification/error', error);
return;
}
editorSvc.clEditor.replaceAll(`[图片上传中...(image-${imgId})]`, `![输入图片说明](${url})`);
} catch (err) {
console.error(err); // eslint-disable-line no-console
editorSvc.clEditor.replaceAll(`[图片上传中...(image-${imgId})]`, `[图片上传失败...(image-${imgId})]`);
store.dispatch('notification/error', err);
}
},
},
mounted() {

View File

@ -1,18 +1,20 @@
<template>
<modal-inner aria-label="插入图像">
<div class="modal__content">
<p v-if="hasFile">
<span v-if="uploading">粘贴/拖拽图片上传中...</span>
<span v-if="!this.uploading && url">
<img :src="url">
</span>
<span v-if="!this.uploading && !url">图片上传失败如未添加图床请先添加并选择之后关闭窗口再重试</span>
</p>
<p v-if="!hasFile">请为您的图像提供<b> url </b></p>
<form-entry v-if="!hasFile" label="URL" error="url">
<p>请为您的图像提供<b> url </b><span v-if="uploading">(图片上传中...)</span></p>
<form-entry label="URL" error="url">
<input slot="field" class="textfield" type="text" v-model.trim="url" @keydown.enter="resolve">
</form-entry>
<p>添加并选择图床后可实现粘贴/拖拽自动上传图片</p>
</div>
<div class="modal__button-bar">
<input class="hidden-file" id="upload-image-file-input" type="file" accept="image/*" :disabled="uploading" @change="uploadImage">
<label for="upload-image-file-input"><a class="button">上传图片</a></label>
<button class="button" @click="reject()">取消</button>
<button class="button button--resolve" @click="resolve" :disabled="uploading">确认</button>
</div>
<div>
<hr />
<p>添加并选择图床后可在编辑区中粘贴/拖拽图片自动上传</p>
<menu-entry @click.native="checkedImgDest(token.sub, token.providerId)" v-for="token in imageTokens" :key="token.sub">
<icon-check-circle v-if="checkedStorage.sub === token.sub" slot="icon"></icon-check-circle>
<icon-check-circle-un v-if="checkedStorage.sub !== token.sub" slot="icon"></icon-check-circle-un>
@ -30,9 +32,9 @@
<span class="line-entry" v-if="token.params">自定义Form参数{{token.params}}</span>
</menu-item>
</menu-entry>
<menu-entry @click.native="checkedImgDest(tokenStorage.sid, tokenStorage.providerId)" v-for="tokenStorage in tokensImgStorages" :key="tokenStorage.sid">
<icon-check-circle v-if="checkedStorage.sub === tokenStorage.sid" slot="icon"></icon-check-circle>
<icon-check-circle-un v-if="checkedStorage.sub !== tokenStorage.sid" slot="icon"></icon-check-circle-un>
<menu-entry @click.native="checkedImgDest(tokenStorage.token.sub, tokenStorage.providerId, tokenStorage.sid)" v-for="tokenStorage in tokensImgStorages" :key="tokenStorage.sid">
<icon-check-circle v-if="checkedStorage.sid === tokenStorage.sid" slot="icon"></icon-check-circle>
<icon-check-circle-un v-if="checkedStorage.sid !== tokenStorage.sid" slot="icon"></icon-check-circle-un>
<menu-item>
<icon-provider slot="icon" :provider-id="tokenStorage.providerId"></icon-provider>
<div>{{tokenStorage.providerName}}
@ -43,12 +45,6 @@
<span> {{tokenStorage.uname}}, 仓库URL: {{tokenStorage.repoUrl}}, 路径: {{tokenStorage.path}}, 分支: {{tokenStorage.branch}}</span>
</menu-item>
</menu-entry>
</div>
<div class="modal__button-bar">
<button class="button" @click="reject()">取消</button>
<button class="button button--resolve" @click="resolve" :disabled="uploading">确认</button>
</div>
<div>
<menu-entry @click.native="addSmmsAccount">
<icon-provider slot="icon" provider-id="smms"></icon-provider>
<span>添加SM.MS图床账号</span>
@ -79,6 +75,7 @@ import giteaHelper from '../../services/providers/helpers/giteaHelper';
import githubHelper from '../../services/providers/helpers/githubHelper';
import customHelper from '../../services/providers/helpers/customHelper';
import utils from '../../services/utils';
import imageSvc from '../../services/imageSvc';
export default modalTemplate({
components: {
@ -86,7 +83,6 @@ export default modalTemplate({
MenuItem,
},
data: () => ({
hasFile: false,
uploading: false,
url: '',
}),
@ -137,92 +133,12 @@ export default modalTemplate({
uname: it.token.name,
providerId: it.providerId,
providerName: it.providerName,
repoUrl: it.providerId === 'gitea' ? `${it.serverUrl}/${storage.repoUri}` : `${storage.owner}/${storage.repo}`,
repoUrl: it.providerId === 'gitea' ? `${it.token.serverUrl}/${storage.repoUri}` : `${storage.owner}/${storage.repo}`,
}));
});
return imgStorages;
},
},
async mounted() {
this.hasFile = false;
const imgFile = store.getters['img/getImg'];
if (imgFile) {
this.hasFile = true;
this.uploading = true;
try {
//
// provider smms
const currStorage = this.checkedStorage;
if (!currStorage) {
store.dispatch('notification/info', '暂无已选择的图床,未自动上传图片!请选择图床后重新粘贴/拖拽图片!');
return;
}
if (currStorage.provider === 'smms' || currStorage.provider === 'custom') {
const filterTokens = this.imageTokens.filter(it => it.sub === currStorage.sub);
if (!filterTokens.length) {
store.dispatch('notification/info', '图床已失效,未自动上传图片!请选择图床后重新粘贴/拖拽图片!');
return;
}
const token = filterTokens[0];
const helper = currStorage.provider === 'smms' ? smmsHelper : customHelper;
try {
this.url = await helper.uploadFile({
token,
file: imgFile,
});
} catch (err) {
store.dispatch('notification/error', err);
}
} else if (currStorage.provider === 'gitea' || currStorage.provider === 'github') {
const filterTokenStorages = this.tokensImgStorages
.filter(it => it.sid === currStorage.sub);
if (!filterTokenStorages.length) {
store.dispatch('notification/info', 'Gitea图床已失效未自动上传图片请选择图床后重新粘贴/拖拽图片!');
return;
}
const tokenStorage = filterTokenStorages[0];
const time = new Date();
const date = time.getDate();
const month = time.getMonth() + 1;
const year = time.getFullYear();
let path = tokenStorage.path.replace('{YYYY}', year)
.replace('{MM}', `0${month}`.slice(-2)).replace('{DD}', `0${date}`.slice(-2));
path = `${path}${path.endsWith('/') ? '' : '/'}${utils.uid()}.${imgFile.type.split('/')[1]}`;
try {
if (currStorage.provider === 'gitea') {
const result = await giteaHelper.uploadFile({
token: tokenStorage.token,
projectId: tokenStorage.repoUri,
branch: tokenStorage.branch,
path,
content: imgFile,
isFile: true,
});
this.url = result.content.download_url;
} else if (currStorage.provider === 'github') {
const result = await githubHelper.uploadFile({
token: tokenStorage.token,
owner: tokenStorage.owner,
repo: tokenStorage.repo,
branch: tokenStorage.branch,
path,
content: imgFile,
isFile: true,
});
this.url = result.content.download_url;
}
} catch (err) {
store.dispatch('notification/error', err);
}
} else {
store.dispatch('notification/info', '暂无已选择的图床,未自动上传图片!请选择图床后重新粘贴/拖拽图片!');
}
} finally {
store.dispatch('img/clearImg');
this.uploading = false;
}
}
},
methods: {
resolve(evt) {
evt.preventDefault(); // Fixes https://github.com/mafgwo/stackedit/issues/1503
@ -239,6 +155,27 @@ export default modalTemplate({
this.config.reject();
callback(null);
},
async uploadImage(evt) {
if (!evt.target.files || !evt.target.files.length) {
return;
}
const imgFile = evt.target.files[0];
try {
this.uploading = true;
const { url, error } = await imageSvc.updateImg(imgFile);
if (error) {
store.dispatch('notification/error', error);
return;
}
this.url = url;
} catch (err) {
store.dispatch('notification/error', err);
} finally {
this.uploading = false;
//
evt.target.value = '';
}
},
async remove(proivderId, item) {
try {
await store.dispatch('modal/open', 'imgStorageDeletion');
@ -288,7 +225,7 @@ export default modalTemplate({
githubHelper.updateToken(token, imgStorageInfo);
} catch (e) { /* Cancel */ }
},
async checkedImgDest(sub, provider) {
async checkedImgDest(sub, provider, sid) {
let type = 'token';
if (provider === 'gitea' || provider === 'github') {
type = 'tokenRepo';
@ -297,6 +234,7 @@ export default modalTemplate({
type,
provider,
sub,
sid,
});
// const { callback } = this.config;
// this.config.reject();

View File

@ -1,78 +1,78 @@
Headers
标题
---------------------------
# Header 1
# 标题1
## Header 2
## 标题2
### Header 3
### 标题3
Styling
样式
---------------------------
*Emphasize* _emphasize_
*强调* _强调_
**Strong** __strong__
**加粗** __加粗__
==Marked text.==
==标记文本==
~~Mistaken text.~~
~~删除线文本~~
> Quoted text.
> 块引用文本
H~2~O is a liquid.
H~2~O是一种液体
2^10^ is 1024.
2^10^是1024
Lists
列表
---------------------------
- Item
* Item
+ Item
- 列表项
* 列表项
+ 列表项
1. Item 1
2. Item 2
3. Item 3
1. 列表项 1
2. 列表项 2
3. 列表项 3
- [ ] Incomplete item
- [x] Complete item
- [ ] 未完成项
- [x] 已完成项
Links
链接
---------------------------
A [link](http://example.com).
一个[链接](http://example.com).
An image: ![Alt](img.jpg)
一张图片: ![图片描述](img.jpg)
A sized image: ![Alt](img.jpg =60x50)
一张调整大小的图片: ![图片描述](img.jpg =60x50)
Code
代码
---------------------------
Some `inline code`.
一些`行内代码`.
```
// A code block
// 一个代码块
var foo = 'bar';
```
```javascript
// An highlighted block
// 一个高亮代码块
var foo = 'bar';
```
Tables
表格
---------------------------
Item | Value
@ -88,41 +88,41 @@ Pipe | $1
Definition lists
定义列表
---------------------------
Markdown
: Text-to-HTML conversion tool
: 文本到HTML转换工具
Authors
: John
: Luke
作者
: 张三
: 李四
Footnotes
脚注
---------------------------
Some text with a footnote.[^1]
一些带有脚注的文本。[^1]
[^1]: The footnote.
[^1]: 脚注内容。
Abbreviations
缩写
---------------------------
Markdown converts text to HTML.
Markdown将文本转换为 HTML。
*[HTML]: HyperText Markup Language
*[HTML]: 超文本标记语言
LaTeX math
LaTeX数学表达式
---------------------------
The Gamma function satisfying $\Gamma(n) = (n-1)!\quad\forall
n\in\mathbb N$ is via the Euler integral
满足 $\Gamma(n) = (n-1)!\quad\forall
n\in\mathbb N$ 的Gamma函数是通过欧拉积分
$$
\Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,.

View File

@ -6,34 +6,34 @@ var util = {},
var defaultsStrings = {
bold: "Strong <strong> Ctrl/Cmd+B",
boldexample: "strong text",
boldexample: "加粗文本",
italic: "Emphasis <em> Ctrl/Cmd+I",
italicexample: "emphasized text",
italicexample: "强调文本",
strikethrough: "Strikethrough <s> Ctrl/Cmd+I",
strikethroughexample: "strikethrough text",
strikethroughexample: "删除线文本",
link: "Hyperlink <a> Ctrl/Cmd+L",
linkdescription: "enter link description here",
linkdescription: "这里输入链接描述",
linkdialog: "<p><b>Insert Hyperlink</b></p><p>http://example.com/ \"optional title\"</p>",
quote: "Blockquote <blockquote> Ctrl/Cmd+Q",
quoteexample: "Blockquote",
quoteexample: "块引用",
code: "Code Sample <pre><code> Ctrl/Cmd+K",
codeexample: "enter code here",
codeexample: "这里输入代码",
image: "Image <img> Ctrl/Cmd+G",
imagedescription: "",
imagedescription: "输入图片说明",
imagedialog: "<p><b>Insert Image</b></p><p>http://example.com/images/diagram.jpg \"optional title\"<br><br>Need <a href='http://www.google.com/search?q=free+image+hosting' target='_blank'>free image hosting?</a></p>",
olist: "Numbered List <ol> Ctrl/Cmd+O",
ulist: "Bulleted List <ul> Ctrl/Cmd+U",
litem: "List item",
litem: "这里是列表文本",
heading: "Heading <h1>/<h2> Ctrl/Cmd+H",
headingexample: "Heading",
headingexample: "标题",
hr: "Horizontal Rule <hr> Ctrl/Cmd+R",
@ -125,6 +125,8 @@ function Pagedown(options) {
* image url (or null if the user cancelled). If this hook returns false, the default dialog will be used.
*/
hooks.addFalse("insertLinkDialog");
// 插入图片占位字符
hooks.addFalse("insertImageUploading");
var that = this,
input;
@ -463,6 +465,7 @@ function UIManager(input, commandManager) {
buttons.bold = bindCommand("doBold");
buttons.italic = bindCommand("doItalic");
buttons.strikethrough = bindCommand("doStrikethrough");
buttons.imageUploading = bindCommand("doImageUploading");
buttons.link = bindCommand(function (chunk, postProcessing) {
return this.doLinkOrImage(chunk, postProcessing, false);
});
@ -615,6 +618,17 @@ commandProto.doStrikethrough = function (chunk, postProcessing) {
return;
};
commandProto.doImageUploading = function (chunk, postProcessing) {
var enteredCallback = function (imgId) {
if (imgId !== null) {
chunk.before = `${chunk.before}[图片上传中...(image-${imgId})]`;
chunk.selection = '';
}
postProcessing();
};
this.hooks.insertImageUploading(enteredCallback);
}
commandProto.stripLinkDefs = function (text, defsToAdd) {
text = text.replace(/^[ ]{0,3}\[(\d+)\]:[ \t]*\n?[ \t]*<?(\S+?)>?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|$)/gm,
@ -983,37 +997,15 @@ commandProto.doCode = function (chunk) {
// Use 'four space' markdown if the selection is on its own
// line or is multiline.
if ((!hasTextAfter && !hasTextBefore) || /\n/.test(chunk.selection)) {
chunk.before = chunk.before.replace(/[ ]{4}$/,
function (totalMatch) {
chunk.selection = totalMatch + chunk.selection;
return "";
});
var nLinesBack = 1;
var nLinesForward = 1;
if (/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) {
nLinesBack = 0;
}
if (/^\n(\t|[ ]{4,})/.test(chunk.after)) {
nLinesForward = 0;
}
chunk.skipLines(nLinesBack, nLinesForward);
if (!chunk.selection) {
chunk.startTag = " ";
chunk.selection = this.getString("codeexample");
if (/[\n]+```\n$/.test(chunk.before) && /^\n```[ ]*\n/.test(chunk.after)) {
chunk.before = chunk.before.replace(/```\n$/, "");
chunk.after = chunk.after.replace(/^\n```/, "");
} else {
if (/^[ ]{0,3}\S/m.test(chunk.selection)) {
if (/\n/.test(chunk.selection))
chunk.selection = chunk.selection.replace(/^/gm, " ");
else // if it's not multiline, do not select the four added spaces; this is more consistent with the doList behavior
chunk.before += " ";
} else {
chunk.selection = chunk.selection.replace(/^(?:[ ]{4}|[ ]{0,3}\t)/gm, "");
}
chunk.before += '```\n';
chunk.after = '\n```' + chunk.after;
}
if (!chunk.selection) {
chunk.selection = this.getString("codeexample");
}
} else {
// Use backticks (`) to delimit the code block.

View File

@ -383,7 +383,10 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
});
return true;
});
this.pagedownEditor.hooks.set('insertImageUploading', (callback) => {
callback(store.getters['img/currImgId']);
return true;
});
this.editorElt.parentNode.addEventListener('scroll', () => this.saveContentState(true));
this.previewElt.parentNode.addEventListener('scroll', () => this.saveContentState(true));

67
src/services/imageSvc.js Normal file
View File

@ -0,0 +1,67 @@
import store from '../store';
import utils from './utils';
import smmsHelper from '../services/providers/helpers/smmsHelper';
import giteaHelper from '../services/providers/helpers/giteaHelper';
import githubHelper from '../services/providers/helpers/githubHelper';
import customHelper from '../services/providers/helpers/customHelper';
export default {
// 上传图片 返回图片链接
// { url: 'http://xxxx', error: 'xxxxxx'}
async updateImg(imgFile) {
// 操作图片上传
const currStorage = store.getters['img/getCheckedStorage'];
if (!currStorage || !currStorage.provider) {
return { error: '暂无已选择的图床!' };
}
const token = store.getters[`data/${currStorage.provider}TokensBySub`][currStorage.sub];
if (!token) {
return { error: '暂无已选择的图床!' };
}
let url = '';
// token图床类型
if (currStorage.type === 'token') {
const helper = currStorage.provider === 'smms' ? smmsHelper : customHelper;
url = await helper.uploadFile({
token,
file: imgFile,
});
} else if (currStorage.type === 'tokenRepo') { // git repo图床类型
const checkStorages = token.imgStorages.filter(it => it.sid === currStorage.sid);
if (!checkStorages || checkStorages.length === 0) {
return { error: '暂无已选择的图床!' };
}
const checkStorage = checkStorages[0];
const time = new Date();
const date = time.getDate();
const month = time.getMonth() + 1;
const year = time.getFullYear();
let path = checkStorage.path.replace('{YYYY}', year)
.replace('{MM}', `0${month}`.slice(-2)).replace('{DD}', `0${date}`.slice(-2));
path = `${path}${path.endsWith('/') ? '' : '/'}${utils.uid()}.${imgFile.type.split('/')[1]}`;
if (currStorage.provider === 'gitea') {
const result = await giteaHelper.uploadFile({
token,
projectId: checkStorage.repoUri,
branch: checkStorage.branch,
path,
content: imgFile,
isFile: true,
});
url = result.content.download_url;
} else if (currStorage.provider === 'github') {
const result = await githubHelper.uploadFile({
token,
owner: checkStorage.owner,
repo: checkStorage.repo,
branch: checkStorage.branch,
path,
content: imgFile,
isFile: true,
});
url = result.content.download_url;
}
}
return { url };
},
};

View File

@ -5,6 +5,8 @@ export default {
state: {
// 来自粘贴板 或者 拖拽的图片的文件对象
currImg: null,
// 当前图片ID
currImgId: null,
// 选择的存储图床信息
checkedStorage: {
type: null, // 目前存储类型分两种 token 与 tokenRepo
@ -16,6 +18,9 @@ export default {
setNewImg: (state, value) => {
state.currImg = value;
},
setCurrImgId: (state, value) => {
state.currImgId = value;
},
clearCurrImg: (state) => {
state.currImg = null;
},
@ -25,18 +30,21 @@ export default {
type: value.type, // 目前存储类型分两种 token 与 tokenRepo
provider: value.provider, // 对应是何种账号
sub: value.sub, // 对应 token 中的sub
sid: value.sid,
};
} else {
state.checkedStorage = {
type: null, // 目前存储类型分两种 token 与 tokenRepo
provider: null, // 对应是何种账号
sub: null, // 对应 token 中的sub
sid: null,
};
}
},
},
getters: {
getImg: state => state.currImg,
currImgId: state => state.currImgId,
getCheckedStorage: state => state.checkedStorage,
getCheckedStorageSub: state => state.checkedStorage.sub,
},
@ -44,6 +52,9 @@ export default {
setImg({ commit }, img) {
commit('setNewImg', img);
},
setCurrImgId({ commit }, imgId) {
commit('setCurrImgId', imgId);
},
clearImg({ commit }) {
commit('clearCurrImg');
},