From e121b8717e4f65a512a00f191133b0f02a1a5d7e Mon Sep 17 00:00:00 2001 From: lilixxs Date: Thu, 23 Jan 2025 00:23:53 +0800 Subject: [PATCH 1/2] feat: add memos support --- src/common/backend/services/memos/form.tsx | 73 ++++++++++++ src/common/backend/services/memos/index.ts | 17 +++ .../backend/services/memos/interface.ts | 4 + src/common/backend/services/memos/service.ts | 112 ++++++++++++++++++ 4 files changed, 206 insertions(+) create mode 100644 src/common/backend/services/memos/form.tsx create mode 100644 src/common/backend/services/memos/index.ts create mode 100644 src/common/backend/services/memos/interface.ts create mode 100644 src/common/backend/services/memos/service.ts diff --git a/src/common/backend/services/memos/form.tsx b/src/common/backend/services/memos/form.tsx new file mode 100644 index 00000000..3e6a58ec --- /dev/null +++ b/src/common/backend/services/memos/form.tsx @@ -0,0 +1,73 @@ +import { Form } from '@ant-design/compatible'; +import '@ant-design/compatible/assets/index.less'; +import { Input } from 'antd'; +import { FormComponentProps } from '@ant-design/compatible/es/form'; +import React, { Fragment } from 'react'; +import { BaklibBackendServiceConfig } from './interface'; +import useOriginForm from '@/hooks/useOriginForm'; +import { FormattedMessage } from 'react-intl'; + +interface BaklibFormProps { + verified?: boolean; + info?: BaklibBackendServiceConfig; +} + +const FormItem: React.FC = props => { + const { + form, + form: { getFieldDecorator }, + info, + verified, + } = props; + + const { verified: formVerified, handleAuthentication, formRules } = useOriginForm({ + form, + initStatus: !!info, + }); + + let initData: Partial = {}; + if (info) { + initData = info; + } + let editMode = info ? true : false; + return ( + + + {getFieldDecorator('origin', { + initialValue: initData.origin || 'https://www.baklib.com', + rules: [ + { + required: true, + message: 'Host is required!', + }, + ...formRules, + ], + })( + + } + disabled={editMode || formVerified} + onSearch={handleAuthentication} + /> + )} + + + {getFieldDecorator('accessToken', { + initialValue: initData.accessToken, + rules: [ + { + required: true, + message: 'AccessToken is required!', + }, + ], + })()} + + + ); +}; + +export default FormItem; diff --git a/src/common/backend/services/memos/index.ts b/src/common/backend/services/memos/index.ts new file mode 100644 index 00000000..7d464437 --- /dev/null +++ b/src/common/backend/services/memos/index.ts @@ -0,0 +1,17 @@ +import { ServiceMeta } from '../interface'; +import Service from './service'; +import Form from './form'; +import localeService from '@/common/locales'; + +export default (): ServiceMeta => { + return { + name: localeService.format({ + id: 'backend.services.memos.name', + }), + icon: 'memos', + type: 'memos', + service: Service, + form: Form, + homePage: 'https://www.usememos.com/', + }; +}; diff --git a/src/common/backend/services/memos/interface.ts b/src/common/backend/services/memos/interface.ts new file mode 100644 index 00000000..db0c0217 --- /dev/null +++ b/src/common/backend/services/memos/interface.ts @@ -0,0 +1,4 @@ +export interface MemosBackendServiceConfig { + accessToken: string; + origin: string; +} diff --git a/src/common/backend/services/memos/service.ts b/src/common/backend/services/memos/service.ts new file mode 100644 index 00000000..e3324d5d --- /dev/null +++ b/src/common/backend/services/memos/service.ts @@ -0,0 +1,112 @@ +import { DocumentService, CreateDocumentRequest } from '../../index'; +import { extend, RequestMethod } from 'umi-request'; +import md5 from '@web-clipper/shared/lib/md5'; +import { MemosBackendServiceConfig } from './interface'; +import { CompleteStatus } from '../interface'; +import { Repository } from '@/common/backend/services/interface'; + +interface MemosUserResponse { + name: string; + username: string; + email: string; + avatarUrl: string; + description: string; +} + +interface UserInfo { + name: string; + avatar: string; + homePage: string; + description: string; +} + +export default class MemosDocumentService implements DocumentService { + private request: RequestMethod; + private token: string; + private origin: string; + private userInfo: UserInfo | null; + + constructor({ accessToken, origin }: MemosBackendServiceConfig) { + const realHost = origin || 'https://demo.usememos.com'; + this.request = extend({ + prefix: `${realHost}/api/`, + headers: { Authorization: `Bearer ${accessToken}` }, + timeout: 5000, + }); + this.request.interceptors.response.use( + async response => { + if (!response.ok) { + const json = await response.clone().json(); + throw new Error(`(${response.status}) Err_id=${json.code || ''}: ${json.message || '未知错误'}`); + } + return response; + }, + error => { + if (error.response) { + // 服务器返回错误 + return error.response.json().then((json: any) => { + throw new Error(`(${error.response.status}) code=${json.id || ''}: ${json.message || error.message || '未知错误'}`); + }); + } + // (50X)网络错误等 + throw new Error(`(500): ${error.message || '网络错误'}`); + }, + ); + this.token = accessToken; + this.origin = realHost; + this.userInfo = null; + } + + getId = () => { + return '0'; + }; + + getUserInfo = async (): Promise => { + const response = await this.request.post('v1/auth/status'); + + const userInfo: UserInfo = { + name: response.username || 'Memos User', + avatar: response.avatarUrl + ? `${this.origin}${response.avatarUrl}` + : 'https://demo.usememos.com/full-logo.webp', + homePage: this.origin, + description: response.description || 'Memos User', + }; + + this.userInfo = userInfo; + return userInfo; + }; + + + createDocument = async ( + info: CreateDocumentRequest + ): Promise => { + if (!this.userInfo) { + this.userInfo = await this.getUserInfo(); + } + + const response = await this.request.post<{ + id: string; + content: string; + creator: string; + }>('v1/memos', { + data: { + content: info.content, + visibility: 'PRIVATE', + }, + }); + + return { + href: `${this.origin}/u/${this.userInfo.name}`, + }; + }; + + getRepositories = async (): Promise => { + return [{ + id: 'memos_default', + name: '默认分区 Default Repo', + groupId: 'memos', + groupName: '默认分组 Defualt Group', + }]; + }; +} From e7fb52cd7fe3f3007102f75bb317c1ca61e85437 Mon Sep 17 00:00:00 2001 From: lilixxs Date: Fri, 24 Jan 2025 17:27:44 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20memos=20support=20enhance=201.=20?= =?UTF-8?q?=E5=8F=AF=E5=9C=A8=E5=89=AA=E8=97=8F=E6=97=B6=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=20tag=E3=80=82=20new=20memo=20tags=20support=202.=20=E5=A4=9A?= =?UTF-8?q?=E8=AF=AD=E8=A8=80=E6=94=AF=E6=8C=81=EF=BC=8C=E7=9B=AE=E5=89=8D?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E7=AE=80=E4=BD=93=E4=B8=AD=E6=96=87=E5=92=8C?= =?UTF-8?q?=E8=8B=B1=E6=96=87=E3=80=82i18n=20language=20support:=20zh-cn?= =?UTF-8?q?=20and=20en-us=203.=20=E5=8F=AF=E5=9C=A8=E5=89=AA=E8=97=8F?= =?UTF-8?q?=E6=97=B6=E8=AE=BE=E5=AE=9A=E5=8F=AF=E8=A7=81=E6=80=A7=EF=BC=9A?= =?UTF-8?q?=E5=85=AC=E5=BC=80=E7=AC=94=E8=AE=B0/=E7=A7=81=E6=9C=89?= =?UTF-8?q?=E7=AC=94=E8=AE=B0=E3=80=82new=20memo=20visibility=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + src/common/backend/services/memos/form.tsx | 34 ++++++---- .../backend/services/memos/headerForm.tsx | 66 +++++++++++++++++++ src/common/backend/services/memos/index.ts | 4 +- .../backend/services/memos/interface.ts | 30 +++++++++ src/common/backend/services/memos/service.ts | 55 ++++++++-------- src/common/locales/data/zh-CN.json | 12 ++++ 7 files changed, 162 insertions(+), 40 deletions(-) create mode 100644 src/common/backend/services/memos/headerForm.tsx diff --git a/.gitignore b/.gitignore index 10423734..245238dd 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ coverage/ tmp/ .DS_Store .idea +.vscode yarn-error.log dll/ webclipper.zip diff --git a/src/common/backend/services/memos/form.tsx b/src/common/backend/services/memos/form.tsx index 3e6a58ec..4ff4ab0b 100644 --- a/src/common/backend/services/memos/form.tsx +++ b/src/common/backend/services/memos/form.tsx @@ -3,16 +3,16 @@ import '@ant-design/compatible/assets/index.less'; import { Input } from 'antd'; import { FormComponentProps } from '@ant-design/compatible/es/form'; import React, { Fragment } from 'react'; -import { BaklibBackendServiceConfig } from './interface'; +import { MemosBackendServiceConfig } from './interface'; import useOriginForm from '@/hooks/useOriginForm'; import { FormattedMessage } from 'react-intl'; -interface BaklibFormProps { +interface MemosFormProps { verified?: boolean; - info?: BaklibBackendServiceConfig; + info?: MemosBackendServiceConfig; } -const FormItem: React.FC = props => { +const FormItem: React.FC = props => { const { form, form: { getFieldDecorator }, @@ -25,7 +25,7 @@ const FormItem: React.FC = props => { initStatus: !!info, }); - let initData: Partial = {}; + let initData: Partial = {}; if (info) { initData = info; } @@ -34,11 +34,17 @@ const FormItem: React.FC = props => { {getFieldDecorator('origin', { - initialValue: initData.origin || 'https://www.baklib.com', + initialValue: initData.origin || 'https://demo.usememos.com', rules: [ { required: true, - message: 'Host is required!', + message: ( + + ), + type: 'url', }, ...formRules, ], @@ -46,8 +52,8 @@ const FormItem: React.FC = props => { } disabled={editMode || formVerified} @@ -61,10 +67,16 @@ const FormItem: React.FC = props => { rules: [ { required: true, - message: 'AccessToken is required!', + message: ( + ), }, ], - })()} + })()} ); diff --git a/src/common/backend/services/memos/headerForm.tsx b/src/common/backend/services/memos/headerForm.tsx new file mode 100644 index 00000000..68ee0bf0 --- /dev/null +++ b/src/common/backend/services/memos/headerForm.tsx @@ -0,0 +1,66 @@ +import { Input, Tooltip, Select } from 'antd'; +import { Form } from '@ant-design/compatible'; +import '@ant-design/compatible/assets/index.less'; +import { FormComponentProps } from '@ant-design/compatible/lib/form'; +import React, { Fragment } from 'react'; +import locales from '@/common/locales'; +import { VisibilityType } from './interface'; + +const { Option } = Select; + +const HeaderForm: React.FC = ({ form: { getFieldDecorator } }) => { + + return ( + + + + {getFieldDecorator('tags', { + rules: [ + { + pattern: /^(?! )[^\u4e00-\u9fa5~`!@#$%^&*()_+={}\[\]:;"'<>.?\/\\|]*[^\s.,;:!?"'()]*$/, + message: locales.format({ + id: 'backend.services.memos.headerForm.tag_error', + }), + }, + ], + })( + + )} + + + + + {getFieldDecorator('visibility', { + initialValue: VisibilityType[0].value, + })( + + )} + + + ); +}; + +export default HeaderForm; diff --git a/src/common/backend/services/memos/index.ts b/src/common/backend/services/memos/index.ts index 7d464437..2c9e3ed7 100644 --- a/src/common/backend/services/memos/index.ts +++ b/src/common/backend/services/memos/index.ts @@ -2,15 +2,17 @@ import { ServiceMeta } from '../interface'; import Service from './service'; import Form from './form'; import localeService from '@/common/locales'; +import headerForm from './headerForm'; export default (): ServiceMeta => { return { name: localeService.format({ id: 'backend.services.memos.name', }), - icon: 'memos', + icon: '', type: 'memos', service: Service, + headerForm: headerForm, form: Form, homePage: 'https://www.usememos.com/', }; diff --git a/src/common/backend/services/memos/interface.ts b/src/common/backend/services/memos/interface.ts index db0c0217..d01d8aae 100644 --- a/src/common/backend/services/memos/interface.ts +++ b/src/common/backend/services/memos/interface.ts @@ -1,4 +1,34 @@ +import { CreateDocumentRequest } from './../interface'; +import locales from '@/common/locales'; + +export const VisibilityType = [ + { label: () => locales.format({ id: 'backend.services.memos.headerForm.VisibilityType.private', defaultMessage: 'private' }), value: 'PRIVATE' }, + { label: () => locales.format({ id: 'backend.services.memos.headerForm.VisibilityType.public', defaultMessage: 'public' }), value: 'PUBLIC' }, +] as const; + +export type VisibilityType = typeof VisibilityType[number]; + export interface MemosBackendServiceConfig { accessToken: string; origin: string; } + +export interface MemosUserResponse { + name: string; + username: string; + email: string; + avatarUrl: string; + description: string; +} + +export interface MemosUserInfo { + name: string; + avatar: string; + homePage: string; + description: string; +} + +export interface MemoCreateDocumentRequest extends CreateDocumentRequest { + visibility?: VisibilityType; + tags?: string; +} diff --git a/src/common/backend/services/memos/service.ts b/src/common/backend/services/memos/service.ts index e3324d5d..cc8558ef 100644 --- a/src/common/backend/services/memos/service.ts +++ b/src/common/backend/services/memos/service.ts @@ -1,30 +1,20 @@ -import { DocumentService, CreateDocumentRequest } from '../../index'; +import { DocumentService } from '../../index'; import { extend, RequestMethod } from 'umi-request'; -import md5 from '@web-clipper/shared/lib/md5'; -import { MemosBackendServiceConfig } from './interface'; import { CompleteStatus } from '../interface'; import { Repository } from '@/common/backend/services/interface'; +import { + MemosBackendServiceConfig, + MemoCreateDocumentRequest, + MemosUserResponse, + MemosUserInfo +} from './interface'; -interface MemosUserResponse { - name: string; - username: string; - email: string; - avatarUrl: string; - description: string; -} - -interface UserInfo { - name: string; - avatar: string; - homePage: string; - description: string; -} export default class MemosDocumentService implements DocumentService { private request: RequestMethod; private token: string; private origin: string; - private userInfo: UserInfo | null; + private UserInfo: MemosUserInfo | null; constructor({ accessToken, origin }: MemosBackendServiceConfig) { const realHost = origin || 'https://demo.usememos.com'; @@ -54,17 +44,17 @@ export default class MemosDocumentService implements DocumentService { ); this.token = accessToken; this.origin = realHost; - this.userInfo = null; + this.UserInfo = null; } getId = () => { return '0'; }; - getUserInfo = async (): Promise => { + getUserInfo = async (): Promise => { const response = await this.request.post('v1/auth/status'); - const userInfo: UserInfo = { + const MemosUserInfo: MemosUserInfo = { name: response.username || 'Memos User', avatar: response.avatarUrl ? `${this.origin}${response.avatarUrl}` @@ -73,16 +63,25 @@ export default class MemosDocumentService implements DocumentService { description: response.description || 'Memos User', }; - this.userInfo = userInfo; - return userInfo; + this.UserInfo = MemosUserInfo; + return MemosUserInfo; }; + private addTag = (tags: string, content: string): string => { + const tagArray = tags.split(',').map(tag => tag.trim()).filter(tag => tag); + const formattedTags = tagArray.map(tag => `#${tag}`).join(' '); + return `${content}\n${formattedTags}`; + }; createDocument = async ( - info: CreateDocumentRequest + info: MemoCreateDocumentRequest ): Promise => { - if (!this.userInfo) { - this.userInfo = await this.getUserInfo(); + if (!this.UserInfo) { + this.UserInfo = await this.getUserInfo(); + } + + if (info.tags) { + info.content = this.addTag(info.tags, info.content); } const response = await this.request.post<{ @@ -92,12 +91,12 @@ export default class MemosDocumentService implements DocumentService { }>('v1/memos', { data: { content: info.content, - visibility: 'PRIVATE', + visibility: info.visibility || 'PRIVATE', }, }); return { - href: `${this.origin}/u/${this.userInfo.name}`, + href: `${this.origin}/u/${this.UserInfo.name}`, }; }; diff --git a/src/common/locales/data/zh-CN.json b/src/common/locales/data/zh-CN.json index 6a5fadcc..3b6ade14 100644 --- a/src/common/locales/data/zh-CN.json +++ b/src/common/locales/data/zh-CN.json @@ -25,6 +25,18 @@ "backend.imageHosting.wiznote.name": "为知笔记", "backend.imageHosting.wiznote.builtInRemark": "为知笔记内置图床", "backend.not.unavailable": "暂时无法剪辑此类型的页面。\n\n刷新页面可以解决。", + + "backend.services.memos.name": "Memos", + "backend.services.memos.form.hostTest": "检验", + "backend.services.memos.accessToken.message": "请输入 AccessToken", + "backend.services.memos.form.authentication": "请输入服务器地址", + "backend.services.memos.headerForm.tag": "请输入标签名称,多个标签用英文逗号分隔,如 tag1,tag2...", + "backend.services.memos.headerForm.visibility": "文档类型", + "backend.services.memos.headerForm.VisibilityType.private": "私人", + "backend.services.memos.headerForm.VisibilityType.public": "公开", + "backend.services.memos.headerForm.tag_error": "标签格式错误,请检查", + + "backend.services.baklib.form.hostTest": "测试", "backend.services.baklib.form.authentication": "授权", "backend.services.baklib.headerForm.channel": "栏目", "backend.services.baklib.headerForm.description": "描述",