mirror of
https://github.com/Molunerfinn/PicGo.git
synced 2026-09-20 03:16:37 +00:00
✨ Feature: add global url rewrite support (#1377)
ISSUES CLOSED: #1255, #1281
This commit is contained in:
+1
-1
@@ -53,7 +53,7 @@
|
||||
"mime-types": "^3.0.2",
|
||||
"mitt": "^3.0.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"picgo": "^1.6.4",
|
||||
"picgo": "^1.8.1",
|
||||
"qrcode.vue": "^3.3.3",
|
||||
"semver": "^7.7.3",
|
||||
"shell-path": "2.1.0",
|
||||
|
||||
Generated
+22
-943
File diff suppressed because it is too large
Load Diff
+55
-5
@@ -68,6 +68,7 @@ SETTINGS_SET_LOG_FILE: Set Log File
|
||||
SETTINGS_CLICK_TO_SET: Click to Set
|
||||
SETTINGS_CLICK_TO_CHECK: Click to Check
|
||||
SETTINGS_SET_SHORTCUT: Set Shortcut
|
||||
SETTINGS_URL_REWRITE: URL Rewrite
|
||||
SETTINGS_CUSTOM_LINK_FORMAT: Custom Link Format
|
||||
SETTINGS_SET_PROXY_AND_MIRROR: Set Proxy and Mirror
|
||||
SETTINGS_SET_SERVER: Set Server
|
||||
@@ -118,6 +119,46 @@ SETTINGS_USE_BUILTIN_CLIPBOARD_UPLOAD: Use Builtin Clipboard to Upload
|
||||
SETTINGS_CHOOSE_LANGUAGE: Choose Language
|
||||
UPLOADER_CONFIG_NAME: Configuration Name
|
||||
BUILTIN_CLIPBOARD_TIPS: Use builtin clipboard function to upload instead of using scripts
|
||||
|
||||
# url rewrite
|
||||
|
||||
URL_REWRITE_HELP: Rewrites uploaded image URLs. Rules are evaluated in order; the first matched rule wins.
|
||||
URL_REWRITE_ADD_RULE: Add Rule
|
||||
URL_REWRITE_EDIT_RULE: Edit Rule
|
||||
URL_REWRITE_EMPTY: No rules
|
||||
URL_REWRITE_ORDER: Order
|
||||
URL_REWRITE_MATCH: Match
|
||||
URL_REWRITE_REPLACE: Replace
|
||||
URL_REWRITE_FLAGS: Flags
|
||||
URL_REWRITE_ENABLED: Enabled
|
||||
URL_REWRITE_ACTIONS: Actions
|
||||
URL_REWRITE_MOVE_UP: Up
|
||||
URL_REWRITE_MOVE_DOWN: Down
|
||||
URL_REWRITE_EDIT: Edit
|
||||
URL_REWRITE_DELETE: Delete
|
||||
URL_REWRITE_DELETE_CONFIRM: Delete this rule?
|
||||
URL_REWRITE_MATCH_TIPS: Supports regex (JavaScript RegExp)
|
||||
URL_REWRITE_MATCH_PLACEHOLDER: https://example.com/path
|
||||
URL_REWRITE_REPLACE_TIPS: Replacement string (supports $1, $2, ...)
|
||||
URL_REWRITE_REPLACE_PLACEHOLDER: https://example.org/newpath
|
||||
URL_REWRITE_OPTIONS: Options
|
||||
URL_REWRITE_RULE_ENABLED: Enable this rule
|
||||
URL_REWRITE_FLAG_GLOBAL_LABEL: Global (g)
|
||||
URL_REWRITE_FLAG_GLOBAL_DESC: Replace all occurrences, not just the first one
|
||||
URL_REWRITE_FLAG_IGNORE_CASE_LABEL: Ignore case (i)
|
||||
URL_REWRITE_FLAG_IGNORE_CASE_DESC: Case-insensitive matching (e.g. JPG equals jpg)
|
||||
URL_REWRITE_MATCH_REQUIRED: Match is required
|
||||
URL_REWRITE_REPLACE_REQUIRED: Replace is required
|
||||
URL_REWRITE_INVALID_REGEX: Invalid regex
|
||||
URL_REWRITE_PREVIEW_TITLE: Preview
|
||||
URL_REWRITE_PREVIEW_TIPS: Enter a URL to see how the current rules rewrite it (matched in order; only the first match is applied)
|
||||
URL_REWRITE_PREVIEW_PLACEHOLDER: https://example.com/path/to/image.png
|
||||
URL_REWRITE_PREVIEW_RUN: Preview
|
||||
URL_REWRITE_PREVIEW_OUTPUT: Output URL
|
||||
URL_REWRITE_PREVIEW_INPUT_REQUIRED: Please enter a URL to preview
|
||||
URL_REWRITE_PREVIEW_RULE_INVALID: Invalid rule
|
||||
URL_REWRITE_PREVIEW_MATCHED_RULE: Matched rule
|
||||
URL_REWRITE_PREVIEW_NO_MATCH: No rules matched
|
||||
UPLOADER_CONFIG_PLACEHOLDER: Please Enter Configuration Name
|
||||
SELECTED_SETTING_HINT: Selected
|
||||
SETTINGS_ENCODE_OUTPUT_URL: Encode Output(or Copyed) URL
|
||||
@@ -144,11 +185,20 @@ SHORTCUT_EDIT: Edit
|
||||
SHORTCUT_CHANGE_UPLOAD: Change Upload Shortcut
|
||||
|
||||
# gallery-page
|
||||
CHANGE_IMAGE_URL_HOST: Change Image URL HOST
|
||||
NEW_IMAGE_URL_HOST: New Image URL HOST
|
||||
SELECTED_IMAGE_URL_HOST: Selected Image URL HOST
|
||||
CHANGE_IMAGE_URL_HOST_RESULT: The result of changing image URL HOST
|
||||
CHANGE_IMAGE_URL_HOST_WARN: You must select at least one picture first
|
||||
GALLERY_URL_REWRITE_TITLE: Rewrite Selected Image URLs
|
||||
GALLERY_URL_REWRITE_RESULT_TITLE: Rewrite Image URL Result
|
||||
GALLERY_URL_REWRITE_WARN_NO_SELECTION: You must select at least one picture first
|
||||
|
||||
GALLERY_URL_REWRITE_APPLY_GLOBAL_RULES: Apply global URL rewrite rules
|
||||
GALLERY_URL_REWRITE_GLOBAL_RULES_COUNT: Global rules
|
||||
GALLERY_URL_REWRITE_TEMP_RULE_TIPS: Optional. Leave empty to skip the temporary rule. Temporary rule has higher priority than global rules.
|
||||
GALLERY_URL_REWRITE_TEMP_RULE_REQUIRED: Match and Replace are required for the temporary rule
|
||||
GALLERY_URL_REWRITE_NO_RULES_TO_APPLY: No global rules enabled and no temporary rule provided
|
||||
GALLERY_URL_REWRITE_SAVE_TEMP_RULE_PROMPT: Save the temporary rule to global URL rewrite rules?
|
||||
GALLERY_URL_REWRITE_APPLY_AND_SAVE: Apply and Save
|
||||
GALLERY_URL_REWRITE_APPLY_ONLY: Apply Only
|
||||
GALLERY_URL_REWRITE_NO_CHANGES: No URLs were changed
|
||||
GALLERY_URL_REWRITE_EMPTY_RESULT_WARN: The rewrite result is empty; skipped
|
||||
|
||||
# tray-page
|
||||
|
||||
|
||||
+55
-5
@@ -68,6 +68,7 @@ SETTINGS_SET_LOG_FILE: 设置日志文件
|
||||
SETTINGS_CLICK_TO_SET: 点击设置
|
||||
SETTINGS_CLICK_TO_CHECK: 点击检查
|
||||
SETTINGS_SET_SHORTCUT: 设置快捷键
|
||||
SETTINGS_URL_REWRITE: URL 重写
|
||||
SETTINGS_CUSTOM_LINK_FORMAT: 自定义链接格式
|
||||
SETTINGS_SET_PROXY_AND_MIRROR: 设置代理和镜像地址
|
||||
SETTINGS_SET_SERVER: 设置Server
|
||||
@@ -118,6 +119,46 @@ SETTINGS_USE_BUILTIN_CLIPBOARD_UPLOAD: 使用内置剪贴板上传
|
||||
SETTINGS_CHOOSE_LANGUAGE: 选择语言
|
||||
BUILTIN_CLIPBOARD_TIPS: 使用内置剪贴板函数而不是调用脚本获取剪贴板图片
|
||||
UPLOADER_CONFIG_NAME: 图床配置名
|
||||
|
||||
# url rewrite
|
||||
|
||||
URL_REWRITE_HELP: 用于重写上传后的图片 URL。规则按顺序匹配,命中的第一条匹配生效。
|
||||
URL_REWRITE_ADD_RULE: 新增规则
|
||||
URL_REWRITE_EDIT_RULE: 编辑规则
|
||||
URL_REWRITE_EMPTY: 暂无规则
|
||||
URL_REWRITE_ORDER: 顺序
|
||||
URL_REWRITE_MATCH: 匹配
|
||||
URL_REWRITE_REPLACE: 替换
|
||||
URL_REWRITE_FLAGS: 标志
|
||||
URL_REWRITE_ENABLED: 启用
|
||||
URL_REWRITE_ACTIONS: 操作
|
||||
URL_REWRITE_MOVE_UP: 上移
|
||||
URL_REWRITE_MOVE_DOWN: 下移
|
||||
URL_REWRITE_EDIT: 编辑
|
||||
URL_REWRITE_DELETE: 删除
|
||||
URL_REWRITE_DELETE_CONFIRM: 确定删除该规则?
|
||||
URL_REWRITE_MATCH_TIPS: 支持正则(JavaScript RegExp)
|
||||
URL_REWRITE_MATCH_PLACEHOLDER: https://example.com/path
|
||||
URL_REWRITE_REPLACE_TIPS: 替换内容(支持 $1、$2...)
|
||||
URL_REWRITE_REPLACE_PLACEHOLDER: https://example.org/newpath
|
||||
URL_REWRITE_OPTIONS: 选项
|
||||
URL_REWRITE_RULE_ENABLED: 启用该规则
|
||||
URL_REWRITE_FLAG_GLOBAL_LABEL: 全局(g)
|
||||
URL_REWRITE_FLAG_GLOBAL_DESC: 替换所有找到的内容,而不仅仅是第一个
|
||||
URL_REWRITE_FLAG_IGNORE_CASE_LABEL: 忽略大小写(i)
|
||||
URL_REWRITE_FLAG_IGNORE_CASE_DESC: 匹配时不区分字母大小写(例如 JPG 等同于 jpg)
|
||||
URL_REWRITE_MATCH_REQUIRED: 匹配规则不能为空
|
||||
URL_REWRITE_REPLACE_REQUIRED: 替换内容不能为空
|
||||
URL_REWRITE_INVALID_REGEX: 正则表达式不合法
|
||||
URL_REWRITE_PREVIEW_TITLE: 预览
|
||||
URL_REWRITE_PREVIEW_TIPS: 输入一个 URL,查看当前规则的重写结果(按顺序匹配,仅第一条命中的规则生效)
|
||||
URL_REWRITE_PREVIEW_PLACEHOLDER: https://example.com/path/to/image.png
|
||||
URL_REWRITE_PREVIEW_RUN: 预览
|
||||
URL_REWRITE_PREVIEW_OUTPUT: 输出 URL
|
||||
URL_REWRITE_PREVIEW_INPUT_REQUIRED: 请输入要预览的 URL
|
||||
URL_REWRITE_PREVIEW_RULE_INVALID: 规则无效
|
||||
URL_REWRITE_PREVIEW_MATCHED_RULE: 命中规则
|
||||
URL_REWRITE_PREVIEW_NO_MATCH: 没有规则匹配
|
||||
UPLOADER_CONFIG_PLACEHOLDER: 请输入配置名称
|
||||
SELECTED_SETTING_HINT: 已选中
|
||||
SETTINGS_ENCODE_OUTPUT_URL: 输出(复制) URL 时进行转义
|
||||
@@ -144,11 +185,20 @@ SHORTCUT_EDIT: 编辑
|
||||
SHORTCUT_CHANGE_UPLOAD: 修改上传快捷键
|
||||
|
||||
# gallery-page
|
||||
CHANGE_IMAGE_URL_HOST: 修改图片 URL HOST
|
||||
NEW_IMAGE_URL_HOST: 新的图片 URL HOST
|
||||
SELECTED_IMAGE_URL_HOST: 已选中的图片 URL HOST
|
||||
CHANGE_IMAGE_URL_HOST_RESULT: 修改图片 URL HOST 结果
|
||||
CHANGE_IMAGE_URL_HOST_WARN: 你必须先选中至少一张图片
|
||||
GALLERY_URL_REWRITE_TITLE: 重写选中图片 URL
|
||||
GALLERY_URL_REWRITE_RESULT_TITLE: 重写图片 URL 结果
|
||||
GALLERY_URL_REWRITE_WARN_NO_SELECTION: 你必须先选中至少一张图片
|
||||
|
||||
GALLERY_URL_REWRITE_APPLY_GLOBAL_RULES: 应用全局 URL 重写规则
|
||||
GALLERY_URL_REWRITE_GLOBAL_RULES_COUNT: 全局规则数量
|
||||
GALLERY_URL_REWRITE_TEMP_RULE_TIPS: 可选,留空则不使用临时规则(临时规则优先级高于全局规则)。
|
||||
GALLERY_URL_REWRITE_TEMP_RULE_REQUIRED: 临时规则需要同时填写「匹配」和「替换」
|
||||
GALLERY_URL_REWRITE_NO_RULES_TO_APPLY: 没有可用的全局规则,且未填写临时规则
|
||||
GALLERY_URL_REWRITE_SAVE_TEMP_RULE_PROMPT: 是否将临时规则写入全局 URL 重写规则列表?
|
||||
GALLERY_URL_REWRITE_APPLY_AND_SAVE: 应用并写入
|
||||
GALLERY_URL_REWRITE_APPLY_ONLY: 仅应用
|
||||
GALLERY_URL_REWRITE_NO_CHANGES: 没有任何 URL 被修改
|
||||
GALLERY_URL_REWRITE_EMPTY_RESULT_WARN: 重写结果为空,已跳过
|
||||
|
||||
# tray-page
|
||||
|
||||
|
||||
+55
-5
@@ -68,6 +68,7 @@ SETTINGS_SET_LOG_FILE: 設定記錄檔案
|
||||
SETTINGS_CLICK_TO_SET: 點擊設定
|
||||
SETTINGS_CLICK_TO_CHECK: 點擊檢查
|
||||
SETTINGS_SET_SHORTCUT: 設定快捷鍵
|
||||
SETTINGS_URL_REWRITE: URL 重寫
|
||||
SETTINGS_CUSTOM_LINK_FORMAT: 自訂連結格式
|
||||
SETTINGS_SET_PROXY_AND_MIRROR: 設定PROXY和鏡像地址
|
||||
SETTINGS_SET_SERVER: 設定Server
|
||||
@@ -118,6 +119,46 @@ SETTINGS_USE_BUILTIN_CLIPBOARD_UPLOAD: 使用內建剪貼簿上傳
|
||||
SETTINGS_CHOOSE_LANGUAGE: 選擇語言
|
||||
BUILTIN_CLIPBOARD_TIPS: 使用內建剪貼簿函數而不是調用腳本取得剪貼簿內的照片
|
||||
UPLOADER_CONFIG_NAME: 圖床配置名
|
||||
|
||||
# url rewrite
|
||||
|
||||
URL_REWRITE_HELP: 用於重寫上傳後的圖片 URL。規則按順序匹配,命中的第一條匹配生效。
|
||||
URL_REWRITE_ADD_RULE: 新增規則
|
||||
URL_REWRITE_EDIT_RULE: 編輯規則
|
||||
URL_REWRITE_EMPTY: 暫無規則
|
||||
URL_REWRITE_ORDER: 順序
|
||||
URL_REWRITE_MATCH: 匹配
|
||||
URL_REWRITE_REPLACE: 替換
|
||||
URL_REWRITE_FLAGS: 標誌
|
||||
URL_REWRITE_ENABLED: 啟用
|
||||
URL_REWRITE_ACTIONS: 操作
|
||||
URL_REWRITE_MOVE_UP: 上移
|
||||
URL_REWRITE_MOVE_DOWN: 下移
|
||||
URL_REWRITE_EDIT: 編輯
|
||||
URL_REWRITE_DELETE: 刪除
|
||||
URL_REWRITE_DELETE_CONFIRM: 確定刪除該規則?
|
||||
URL_REWRITE_MATCH_TIPS: 支援正則(JavaScript RegExp)
|
||||
URL_REWRITE_MATCH_PLACEHOLDER: https://example.com/path
|
||||
URL_REWRITE_REPLACE_TIPS: 替換內容(支援 $1、$2...)
|
||||
URL_REWRITE_REPLACE_PLACEHOLDER: https://example.org/newpath
|
||||
URL_REWRITE_OPTIONS: 選項
|
||||
URL_REWRITE_RULE_ENABLED: 啟用該規則
|
||||
URL_REWRITE_FLAG_GLOBAL_LABEL: 全域(g)
|
||||
URL_REWRITE_FLAG_GLOBAL_DESC: 替換所有找到的內容,而不僅僅是第一個
|
||||
URL_REWRITE_FLAG_IGNORE_CASE_LABEL: 忽略大小寫(i)
|
||||
URL_REWRITE_FLAG_IGNORE_CASE_DESC: 匹配時不區分字母大小寫(例如 JPG 等同於 jpg)
|
||||
URL_REWRITE_MATCH_REQUIRED: 匹配規則不能為空
|
||||
URL_REWRITE_REPLACE_REQUIRED: 替換內容不能為空
|
||||
URL_REWRITE_INVALID_REGEX: 正則表達式不合法
|
||||
URL_REWRITE_PREVIEW_TITLE: 預覽
|
||||
URL_REWRITE_PREVIEW_TIPS: 輸入一個 URL,查看目前規則的重寫結果(按順序匹配,僅第一條命中的規則生效)
|
||||
URL_REWRITE_PREVIEW_PLACEHOLDER: https://example.com/path/to/image.png
|
||||
URL_REWRITE_PREVIEW_RUN: 預覽
|
||||
URL_REWRITE_PREVIEW_OUTPUT: 輸出 URL
|
||||
URL_REWRITE_PREVIEW_INPUT_REQUIRED: 請輸入要預覽的 URL
|
||||
URL_REWRITE_PREVIEW_RULE_INVALID: 規則無效
|
||||
URL_REWRITE_PREVIEW_MATCHED_RULE: 命中規則
|
||||
URL_REWRITE_PREVIEW_NO_MATCH: 沒有規則匹配
|
||||
UPLOADER_CONFIG_PLACEHOLDER: 請輸入配置名稱
|
||||
SELECTED_SETTING_HINT: 已選中
|
||||
SETTINGS_ENCODE_OUTPUT_URL: 輸出(複製) URL 時進行轉義
|
||||
@@ -144,11 +185,20 @@ SHORTCUT_EDIT: 編輯
|
||||
SHORTCUT_CHANGE_UPLOAD: 修改上傳快捷鍵
|
||||
|
||||
# gallery-page
|
||||
CHANGE_IMAGE_URL_HOST: 修改圖片 URL HOST
|
||||
NEW_IMAGE_URL_HOST: 新的圖片 URL HOST
|
||||
SELECTED_IMAGE_URL_HOST: 已選中的圖片 URL HOST
|
||||
CHANGE_IMAGE_URL_HOST_RESULT: 修改圖片 URL HOST 結果
|
||||
CHANGE_IMAGE_URL_HOST_WARN: 你必須先選中至少一張圖片
|
||||
GALLERY_URL_REWRITE_TITLE: 重寫選中圖片 URL
|
||||
GALLERY_URL_REWRITE_RESULT_TITLE: 重寫圖片 URL 結果
|
||||
GALLERY_URL_REWRITE_WARN_NO_SELECTION: 你必須先選中至少一張圖片
|
||||
|
||||
GALLERY_URL_REWRITE_APPLY_GLOBAL_RULES: 套用全域 URL 重寫規則
|
||||
GALLERY_URL_REWRITE_GLOBAL_RULES_COUNT: 全域規則數量
|
||||
GALLERY_URL_REWRITE_TEMP_RULE_TIPS: 可選,留空則不使用臨時規則(臨時規則優先級高於全域規則)。
|
||||
GALLERY_URL_REWRITE_TEMP_RULE_REQUIRED: 臨時規則需要同時填寫「匹配」和「替換」
|
||||
GALLERY_URL_REWRITE_NO_RULES_TO_APPLY: 沒有可用的全域規則,且未填寫臨時規則
|
||||
GALLERY_URL_REWRITE_SAVE_TEMP_RULE_PROMPT: 是否將臨時規則寫入全域 URL 重寫規則列表?
|
||||
GALLERY_URL_REWRITE_APPLY_AND_SAVE: 套用並寫入
|
||||
GALLERY_URL_REWRITE_APPLY_ONLY: 僅套用
|
||||
GALLERY_URL_REWRITE_NO_CHANGES: 沒有任何 URL 被修改
|
||||
GALLERY_URL_REWRITE_EMPTY_RESULT_WARN: 重寫結果為空,已跳過
|
||||
|
||||
# tray-page
|
||||
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import { logger } from '@picgo/i18n'
|
||||
import { IPicGo } from 'picgo'
|
||||
import { T } from '~/main/i18n'
|
||||
import { getHost, removeProtocolAndSuffix, replaceHost } from '~/main/utils/common'
|
||||
|
||||
export const galleryMenu = () => {
|
||||
return [{
|
||||
label: T('CHANGE_IMAGE_URL_HOST'),
|
||||
async handle (ctx: IPicGo, guiApi: IGuiApi, selectedList: ImgInfo[] = []) {
|
||||
const hostList = [...new Set(selectedList.map((item) => {
|
||||
return getHost(item.imgUrl)
|
||||
}).filter(Boolean) as string[])]
|
||||
if (hostList.length === 0) {
|
||||
guiApi.showNotification({
|
||||
title: T('CHANGE_IMAGE_URL_HOST'),
|
||||
body: T('CHANGE_IMAGE_URL_HOST_WARN')
|
||||
})
|
||||
logger.warn(T('CHANGE_IMAGE_URL_HOST_WARN'))
|
||||
return
|
||||
}
|
||||
const config: IPicGoPluginConfig[] = [
|
||||
{
|
||||
alias: T('SELECTED_IMAGE_URL_HOST'),
|
||||
name: 'selectedHost',
|
||||
type: 'checkbox',
|
||||
choices: hostList.map(item => ({
|
||||
name: item,
|
||||
value: item,
|
||||
checked: true
|
||||
})),
|
||||
required: true
|
||||
},
|
||||
{
|
||||
alias: T('NEW_IMAGE_URL_HOST'),
|
||||
name: 'newHost',
|
||||
type: 'input',
|
||||
message: 'www.example.com',
|
||||
default: '',
|
||||
required: true
|
||||
}
|
||||
]
|
||||
const options: IPicGoPluginShowConfigDialogOption = {
|
||||
title: T('CHANGE_IMAGE_URL_HOST'),
|
||||
config
|
||||
}
|
||||
const res = await guiApi.showConfigDialog<{
|
||||
selectedHost: string[]
|
||||
newHost: string
|
||||
}>(options)
|
||||
|
||||
if (res) {
|
||||
const selectedHost = res.selectedHost
|
||||
const newHost = removeProtocolAndSuffix(res.newHost)
|
||||
const changedList = selectedList.map((item) => {
|
||||
try {
|
||||
const url = new URL(item.imgUrl || '')
|
||||
const host = url.host
|
||||
if (selectedHost.includes(host)) {
|
||||
item.imgUrl = replaceHost(item.imgUrl!, host, newHost)
|
||||
return item
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} catch (e: any) {
|
||||
ctx.log.error(e)
|
||||
return false
|
||||
}
|
||||
}).filter(Boolean) as ImgInfo[]
|
||||
const updateRes = await guiApi.galleryDB.updateMany(changedList)
|
||||
guiApi.showNotification({
|
||||
title: T('CHANGE_IMAGE_URL_HOST_RESULT'),
|
||||
body: `${T('SUCCESS')}: ${updateRes.success} ${T('FAILED')}: ${updateRes.total - updateRes.success}}`
|
||||
})
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
import { logger } from '@picgo/i18n'
|
||||
import { PicGoUtils, type IPicGo } from 'picgo'
|
||||
import { T } from '~/main/i18n'
|
||||
|
||||
interface IUrlRewriteRule {
|
||||
match: string
|
||||
replace: string
|
||||
enable?: boolean
|
||||
global?: boolean
|
||||
ignoreCase?: boolean
|
||||
}
|
||||
|
||||
interface IUrlRewriteDialogResult {
|
||||
applyGlobalRules?: boolean
|
||||
match?: string
|
||||
replace?: string
|
||||
global?: boolean
|
||||
ignoreCase?: boolean
|
||||
}
|
||||
|
||||
function normalizeRules (value: unknown): IUrlRewriteRule[] {
|
||||
if (!Array.isArray(value)) return []
|
||||
return value.map(item => {
|
||||
const raw = (item ?? {}) as Partial<Record<keyof IUrlRewriteRule, unknown>>
|
||||
return {
|
||||
match: String(raw.match ?? ''),
|
||||
replace: String(raw.replace ?? ''),
|
||||
enable: raw.enable === false ? false : true,
|
||||
global: raw.global === true,
|
||||
ignoreCase: raw.ignoreCase === true
|
||||
}
|
||||
}).filter(rule => rule.match.length > 0)
|
||||
}
|
||||
|
||||
function buildFlags (rule: Pick<IUrlRewriteRule, 'global' | 'ignoreCase'>): string {
|
||||
return `${rule.global ? 'g' : ''}${rule.ignoreCase ? 'i' : ''}`
|
||||
}
|
||||
|
||||
function validateRuleOrThrow (rule: IUrlRewriteRule) {
|
||||
if (!rule.match.trim() || !rule.replace.trim()) {
|
||||
throw new Error(T('GALLERY_URL_REWRITE_TEMP_RULE_REQUIRED'))
|
||||
}
|
||||
try {
|
||||
new RegExp(rule.match, buildFlags(rule))
|
||||
} catch (error) {
|
||||
const message = `Invalid URL rewrite regex pattern "${rule.match}": ${error instanceof Error ? error.message : String(error)}`
|
||||
logger.error(message)
|
||||
throw new Error(message)
|
||||
}
|
||||
}
|
||||
|
||||
function applyFirstMatchRewrite (ctx: IPicGo, imgItem: ImgInfo, rules: IUrlRewriteRule[]): ImgInfo {
|
||||
const imgInfo = {
|
||||
imgUrl: imgItem.imgUrl,
|
||||
originImgUrl: imgItem.originImgUrl
|
||||
}
|
||||
PicGoUtils.applyUrlRewriteToImgInfo(imgInfo, rules, {
|
||||
log: {
|
||||
error: (...args: Parameters<IPicGo['log']['error']>) => ctx.log.error(...args),
|
||||
warn: () => ctx.log.warn(T('GALLERY_URL_REWRITE_EMPTY_RESULT_WARN'))
|
||||
}
|
||||
})
|
||||
if (imgInfo.imgUrl === '') return imgItem
|
||||
return imgInfo
|
||||
}
|
||||
|
||||
export const galleryMenu = () => {
|
||||
return [{
|
||||
label: T('GALLERY_URL_REWRITE_TITLE'),
|
||||
async handle (ctx: IPicGo, guiApi: IGuiApi, selectedList: ImgInfo[] = []) {
|
||||
if (!selectedList.length) {
|
||||
guiApi.showNotification({
|
||||
title: T('GALLERY_URL_REWRITE_TITLE'),
|
||||
body: T('GALLERY_URL_REWRITE_WARN_NO_SELECTION')
|
||||
})
|
||||
logger.warn(T('GALLERY_URL_REWRITE_WARN_NO_SELECTION'))
|
||||
return
|
||||
}
|
||||
|
||||
const globalRules = normalizeRules(ctx.getConfig('settings.urlRewrite.rules'))
|
||||
|
||||
const config: IPicGoPluginConfig[] = [
|
||||
{
|
||||
alias: T('GALLERY_URL_REWRITE_APPLY_GLOBAL_RULES'),
|
||||
name: 'applyGlobalRules',
|
||||
type: 'confirm',
|
||||
default: globalRules.length > 0,
|
||||
required: false,
|
||||
confirmText: T('SETTINGS_OPEN'),
|
||||
cancelText: T('SETTINGS_CLOSE'),
|
||||
tips: `${T('GALLERY_URL_REWRITE_GLOBAL_RULES_COUNT')}: ${globalRules.length}`
|
||||
},
|
||||
{
|
||||
alias: T('URL_REWRITE_MATCH'),
|
||||
name: 'match',
|
||||
type: 'input',
|
||||
message: T('URL_REWRITE_MATCH_PLACEHOLDER'),
|
||||
default: '',
|
||||
required: false,
|
||||
tips: `${T('GALLERY_URL_REWRITE_TEMP_RULE_TIPS')}\n\n${T('URL_REWRITE_MATCH_TIPS')}`
|
||||
},
|
||||
{
|
||||
alias: T('URL_REWRITE_REPLACE'),
|
||||
name: 'replace',
|
||||
type: 'input',
|
||||
message: T('URL_REWRITE_REPLACE_PLACEHOLDER'),
|
||||
default: '',
|
||||
required: false,
|
||||
tips: T('URL_REWRITE_REPLACE_TIPS')
|
||||
},
|
||||
{
|
||||
alias: T('URL_REWRITE_FLAG_GLOBAL_LABEL'),
|
||||
name: 'global',
|
||||
type: 'confirm',
|
||||
default: false,
|
||||
required: false,
|
||||
confirmText: 'g',
|
||||
cancelText: '-',
|
||||
tips: T('URL_REWRITE_FLAG_GLOBAL_DESC')
|
||||
},
|
||||
{
|
||||
alias: T('URL_REWRITE_FLAG_IGNORE_CASE_LABEL'),
|
||||
name: 'ignoreCase',
|
||||
type: 'confirm',
|
||||
default: false,
|
||||
required: false,
|
||||
confirmText: 'i',
|
||||
cancelText: '-',
|
||||
tips: T('URL_REWRITE_FLAG_IGNORE_CASE_DESC')
|
||||
}
|
||||
]
|
||||
const options: IPicGoPluginShowConfigDialogOption = {
|
||||
title: T('GALLERY_URL_REWRITE_TITLE'),
|
||||
config
|
||||
}
|
||||
const res = await guiApi.showConfigDialog<IUrlRewriteDialogResult>(options)
|
||||
if (!res) return
|
||||
|
||||
const applyGlobalRules = res.applyGlobalRules === true
|
||||
const tempMatch = String(res.match ?? '').trim()
|
||||
const tempReplace = String(res.replace ?? '').trim()
|
||||
|
||||
const hasTempRuleInput = tempMatch.length > 0 || tempReplace.length > 0
|
||||
let tempRule: IUrlRewriteRule | null = null
|
||||
if (hasTempRuleInput) {
|
||||
tempRule = {
|
||||
match: tempMatch,
|
||||
replace: tempReplace,
|
||||
enable: true,
|
||||
global: res.global === true,
|
||||
ignoreCase: res.ignoreCase === true
|
||||
}
|
||||
}
|
||||
|
||||
if (tempRule) {
|
||||
try {
|
||||
validateRuleOrThrow(tempRule)
|
||||
} catch (e: any) {
|
||||
guiApi.showNotification({
|
||||
title: T('GALLERY_URL_REWRITE_TITLE'),
|
||||
body: e.message
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (!applyGlobalRules && !tempRule) {
|
||||
guiApi.showNotification({
|
||||
title: T('GALLERY_URL_REWRITE_TITLE'),
|
||||
body: T('GALLERY_URL_REWRITE_NO_RULES_TO_APPLY')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
let shouldSaveTempRule = false
|
||||
if (tempRule) {
|
||||
const saveRes = await guiApi.showMessageBox({
|
||||
title: T('GALLERY_URL_REWRITE_TITLE'),
|
||||
message: T('GALLERY_URL_REWRITE_SAVE_TEMP_RULE_PROMPT'),
|
||||
type: 'info',
|
||||
buttons: [
|
||||
T('GALLERY_URL_REWRITE_APPLY_AND_SAVE'),
|
||||
T('GALLERY_URL_REWRITE_APPLY_ONLY'),
|
||||
T('CANCEL')
|
||||
]
|
||||
})
|
||||
if (saveRes.result === 2) return
|
||||
shouldSaveTempRule = saveRes.result === 0
|
||||
}
|
||||
|
||||
const rulesToApply: IUrlRewriteRule[] = [
|
||||
...(tempRule ? [tempRule] : []),
|
||||
...(applyGlobalRules ? globalRules : [])
|
||||
]
|
||||
|
||||
const changedList = selectedList.map((item) => {
|
||||
const current = item.imgUrl || ''
|
||||
if (!current) return false
|
||||
const next = applyFirstMatchRewrite(ctx, item, rulesToApply)
|
||||
if (next.imgUrl === current) return false
|
||||
return {
|
||||
id: item.id,
|
||||
imgUrl: next.imgUrl,
|
||||
originImgUrl: next.originImgUrl
|
||||
} as ImgInfo
|
||||
}).filter(Boolean) as ImgInfo[]
|
||||
|
||||
if (changedList.length === 0) {
|
||||
guiApi.showNotification({
|
||||
title: T('GALLERY_URL_REWRITE_RESULT_TITLE'),
|
||||
body: T('GALLERY_URL_REWRITE_NO_CHANGES')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (shouldSaveTempRule && tempRule) {
|
||||
const nextGlobalRules = [...globalRules, tempRule]
|
||||
ctx.saveConfig({
|
||||
'settings.urlRewrite.rules': nextGlobalRules
|
||||
})
|
||||
}
|
||||
|
||||
const updateRes = await guiApi.galleryDB.updateMany(changedList)
|
||||
guiApi.showNotification({
|
||||
title: T('GALLERY_URL_REWRITE_RESULT_TITLE'),
|
||||
body: `${T('SUCCESS')}: ${updateRes.success} ${T('FAILED')}: ${updateRes.total - updateRes.success}`
|
||||
})
|
||||
}
|
||||
}]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { galleryMenu as changeHostGalleryMenu } from './changeHost'
|
||||
import { galleryMenu as changeURLGalleryMenu } from './changeURL'
|
||||
export const builtInGalleryToolboxMenu = () => {
|
||||
const menuList = [...changeHostGalleryMenu()]
|
||||
const menuList = [...changeURLGalleryMenu()]
|
||||
|
||||
return menuList
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ import { ref } from 'vue'
|
||||
import { marked } from 'marked'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { useVModel } from '@/hooks/useVModel'
|
||||
import { QuestionFilled } from '@element-plus/icons-vue'
|
||||
|
||||
const $form = ref<FormInstance>()
|
||||
|
||||
@@ -117,7 +118,6 @@ async function validate (): Promise<IStringKeyMap | false> {
|
||||
resolve(form.value)
|
||||
} else {
|
||||
resolve(false)
|
||||
return false
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,576 @@
|
||||
<template>
|
||||
<div id="url-rewrite-page">
|
||||
<div class="view-title">
|
||||
{{ $T('SETTINGS_URL_REWRITE') }}
|
||||
</div>
|
||||
<el-row
|
||||
class="url-rewrite-list"
|
||||
justify="center"
|
||||
>
|
||||
<el-col
|
||||
:span="20"
|
||||
:offset="2"
|
||||
>
|
||||
<div class="flex mb-[12px] justify-between align-middle">
|
||||
<div class="text-[12px] text-[#bbb] leading-[18px]">
|
||||
{{ $T('URL_REWRITE_HELP') }}
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="openAddDialog"
|
||||
>
|
||||
<el-icon class="mr-[4px]">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
{{ $T('URL_REWRITE_ADD_RULE') }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
class="url-rewrite-table-border"
|
||||
:data="rules"
|
||||
size="small"
|
||||
header-cell-class-name="url-rewrite-table-border"
|
||||
cell-class-name="url-rewrite-table-border"
|
||||
:empty-text="$T('URL_REWRITE_EMPTY')"
|
||||
>
|
||||
<el-table-column
|
||||
width="50px"
|
||||
:label="$T('URL_REWRITE_ORDER')"
|
||||
>
|
||||
<template #default="scope">
|
||||
{{ scope.$index + 1 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
min-width="300px"
|
||||
:label="$T('URL_REWRITE_MATCH')"
|
||||
>
|
||||
<template #default="scope">
|
||||
<span class="font-mono text-[12px] break-all">
|
||||
{{ scope.row.match }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
width="80px"
|
||||
:label="$T('URL_REWRITE_FLAGS')"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-tag
|
||||
v-if="scope.row.global"
|
||||
size="small"
|
||||
>
|
||||
G
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-if="scope.row.ignoreCase"
|
||||
size="small"
|
||||
class="ml-[4px]"
|
||||
>
|
||||
I
|
||||
</el-tag>
|
||||
<span v-if="!scope.row.global && !scope.row.ignoreCase">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
width="70px"
|
||||
:label="$T('URL_REWRITE_ENABLED')"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-switch
|
||||
v-model="scope.row.enable"
|
||||
@change="handleToggleEnable(scope.$index, $event as boolean)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
width="220px"
|
||||
:label="$T('URL_REWRITE_ACTIONS')"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-row class="mb-[2px]">
|
||||
<el-button
|
||||
size="small"
|
||||
type="text"
|
||||
:disabled="scope.$index === 0"
|
||||
@click="moveRule(scope.$index, scope.$index - 1)"
|
||||
>
|
||||
<el-icon class="mr-[2px]">
|
||||
<ArrowUp />
|
||||
</el-icon>
|
||||
{{ $T('URL_REWRITE_MOVE_UP') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="text"
|
||||
:disabled="scope.$index === rules.length - 1"
|
||||
@click="moveRule(scope.$index, scope.$index + 1)"
|
||||
>
|
||||
<el-icon class="mr-[2px]">
|
||||
<ArrowDown />
|
||||
</el-icon>
|
||||
{{ $T('URL_REWRITE_MOVE_DOWN') }}
|
||||
</el-button>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-button
|
||||
size="small"
|
||||
type="text"
|
||||
@click="openEditDialog(scope.row, scope.$index)"
|
||||
>
|
||||
<el-icon class="mr-[2px]">
|
||||
<Edit />
|
||||
</el-icon>
|
||||
{{ $T('URL_REWRITE_EDIT') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
class="danger"
|
||||
size="small"
|
||||
type="text"
|
||||
@click="confirmDelete(scope.$index)"
|
||||
>
|
||||
<el-icon class="mr-[2px]">
|
||||
<Delete />
|
||||
</el-icon>
|
||||
{{ $T('URL_REWRITE_DELETE') }}
|
||||
</el-button>
|
||||
</el-row>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="mt-[16px] rounded-[8px] url-rewrite-panel p-[12px]">
|
||||
<div class="text-[14px] mb-[8px] text-[#bbb] font-bold">
|
||||
{{ $T('URL_REWRITE_PREVIEW_TITLE') }}
|
||||
</div>
|
||||
<div class="text-[12px] text-[#bbb] leading-[18px] mb-[10px]">
|
||||
{{ $T('URL_REWRITE_PREVIEW_TIPS') }}
|
||||
</div>
|
||||
|
||||
<el-row
|
||||
class="mb-[10px]"
|
||||
:gutter="10"
|
||||
>
|
||||
<el-col :span="18">
|
||||
<el-input
|
||||
v-model="previewInputUrl"
|
||||
:placeholder="$T('URL_REWRITE_PREVIEW_PLACEHOLDER')"
|
||||
size="small"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
class="w-full"
|
||||
@click="runPreview"
|
||||
>
|
||||
{{ $T('URL_REWRITE_PREVIEW_RUN') }}
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-alert
|
||||
v-if="previewStatus === 'error'"
|
||||
:title="previewMessage"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
<el-alert
|
||||
v-else-if="previewStatus === 'matched'"
|
||||
:title="previewMessage"
|
||||
type="success"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
<el-alert
|
||||
v-else-if="previewStatus === 'noMatch'"
|
||||
:title="previewMessage"
|
||||
type="info"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="previewStatus !== 'idle'"
|
||||
class="mt-[10px]"
|
||||
>
|
||||
<div class="text-[12px] text-[#bbb] mb-[6px]">
|
||||
{{ $T('URL_REWRITE_PREVIEW_OUTPUT') }}
|
||||
</div>
|
||||
<div class="rounded-[6px] url-rewrite-mono-box p-[10px] font-mono text-[12px] break-all text-[#bbb]">
|
||||
{{ previewOutputUrl }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-dialog
|
||||
v-model="editDialogVisible"
|
||||
:title="editDialogTitle"
|
||||
:modal-append-to-body="false"
|
||||
>
|
||||
<el-form
|
||||
label-position="top"
|
||||
label-width="80px"
|
||||
size="small"
|
||||
>
|
||||
<el-form-item
|
||||
:label="$T('URL_REWRITE_MATCH')"
|
||||
>
|
||||
<div class="flex flex-col gap-[6px] w-full">
|
||||
<div class="text-[12px] text-[#bbb] leading-[18px]">
|
||||
{{ $T('URL_REWRITE_MATCH_TIPS') }}
|
||||
</div>
|
||||
<el-input
|
||||
v-model="ruleForm.match"
|
||||
class="align-center"
|
||||
:autofocus="true"
|
||||
:placeholder="$T('URL_REWRITE_MATCH_PLACEHOLDER')"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
:label="$T('URL_REWRITE_REPLACE')"
|
||||
>
|
||||
<div class="flex flex-col gap-[6px] w-full">
|
||||
<div class="text-[12px] text-[#bbb] leading-[18px]">
|
||||
{{ $T('URL_REWRITE_REPLACE_TIPS') }}
|
||||
</div>
|
||||
<el-input
|
||||
v-model="ruleForm.replace"
|
||||
class="align-center"
|
||||
:placeholder="$T('URL_REWRITE_REPLACE_PLACEHOLDER')"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
:label="$T('URL_REWRITE_OPTIONS')"
|
||||
>
|
||||
<div class="flex flex-col gap-[10px] w-full">
|
||||
<el-row
|
||||
justify="space-between"
|
||||
align="middle"
|
||||
>
|
||||
<div class="text-[13px]">
|
||||
{{ $T('URL_REWRITE_RULE_ENABLED') }}
|
||||
</div>
|
||||
<el-switch v-model="ruleForm.enable" />
|
||||
</el-row>
|
||||
|
||||
<div class="grid grid-cols-2 gap-[12px]">
|
||||
<div class="rounded-[6px] url-rewrite-option-card p-[10px]">
|
||||
<el-checkbox
|
||||
v-model="ruleForm.global"
|
||||
>
|
||||
{{ $T('URL_REWRITE_FLAG_GLOBAL_LABEL') }}
|
||||
</el-checkbox>
|
||||
<div class="text-[12px] text-[#bbb] leading-[18px] mt-[6px]">
|
||||
{{ $T('URL_REWRITE_FLAG_GLOBAL_DESC') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-[6px] url-rewrite-option-card p-[10px]">
|
||||
<el-checkbox
|
||||
v-model="ruleForm.ignoreCase"
|
||||
>
|
||||
{{ $T('URL_REWRITE_FLAG_IGNORE_CASE_LABEL') }}
|
||||
</el-checkbox>
|
||||
<div class="text-[12px] text-[#bbb] leading-[18px] mt-[6px]">
|
||||
{{ $T('URL_REWRITE_FLAG_IGNORE_CASE_DESC') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button
|
||||
round
|
||||
@click="cancelEditDialog"
|
||||
>
|
||||
{{ $T('CANCEL') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
round
|
||||
@click="confirmEditDialog"
|
||||
>
|
||||
{{ $T('CONFIRM') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ArrowDown, ArrowUp, Delete, Edit, Plus } from '@element-plus/icons-vue'
|
||||
import { ElMessage as $message, ElMessageBox } from 'element-plus'
|
||||
import { computed, onBeforeMount, reactive, ref } from 'vue'
|
||||
import { getConfig, saveConfig } from '@/utils/dataSender'
|
||||
import { T as $T } from '@/i18n'
|
||||
|
||||
interface IUrlRewriteRule {
|
||||
match: string
|
||||
replace: string
|
||||
enable: boolean
|
||||
global: boolean
|
||||
ignoreCase: boolean
|
||||
}
|
||||
|
||||
const $confirm = ElMessageBox.confirm
|
||||
|
||||
const rules = ref<IUrlRewriteRule[]>([])
|
||||
|
||||
const editDialogVisible = ref(false)
|
||||
const editDialogMode = ref<'add' | 'edit'>('add')
|
||||
const editRuleIndex = ref(-1)
|
||||
|
||||
const previewInputUrl = ref('')
|
||||
const previewOutputUrl = ref('')
|
||||
const previewStatus = ref<'idle' | 'matched' | 'noMatch' | 'error'>('idle')
|
||||
const previewMessage = ref('')
|
||||
|
||||
const ruleForm = reactive<IUrlRewriteRule>({
|
||||
match: '',
|
||||
replace: '',
|
||||
enable: true,
|
||||
global: false,
|
||||
ignoreCase: false
|
||||
})
|
||||
|
||||
const editDialogTitle = computed(() => {
|
||||
return editDialogMode.value === 'add' ? $T('URL_REWRITE_ADD_RULE') : $T('URL_REWRITE_EDIT_RULE')
|
||||
})
|
||||
|
||||
onBeforeMount(async () => {
|
||||
await initRules()
|
||||
})
|
||||
|
||||
async function initRules () {
|
||||
const configRules = await getConfig<unknown>('settings.urlRewrite.rules')
|
||||
rules.value = normalizeRules(configRules)
|
||||
}
|
||||
|
||||
function normalizeRules (value: unknown): IUrlRewriteRule[] {
|
||||
if (!Array.isArray(value)) return []
|
||||
return value.map(item => {
|
||||
const raw = (item ?? {}) as Partial<Record<keyof IUrlRewriteRule, unknown>>
|
||||
return {
|
||||
match: String(raw.match ?? ''),
|
||||
replace: String(raw.replace ?? ''),
|
||||
enable: raw.enable === false ? false : true,
|
||||
global: raw.global === true,
|
||||
ignoreCase: raw.ignoreCase === true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function persistRules () {
|
||||
try {
|
||||
await saveConfig('settings.urlRewrite.rules', rules.value)
|
||||
} catch (e) {
|
||||
$message.error($T('OPERATION_FAILED'))
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToggleEnable (index: number, value: boolean) {
|
||||
if (!rules.value[index]) return
|
||||
rules.value[index].enable = value
|
||||
await persistRules()
|
||||
}
|
||||
|
||||
async function moveRule (fromIndex: number, toIndex: number) {
|
||||
if (fromIndex === toIndex) return
|
||||
if (toIndex < 0 || toIndex >= rules.value.length) return
|
||||
const next = [...rules.value]
|
||||
const [item] = next.splice(fromIndex, 1)
|
||||
next.splice(toIndex, 0, item)
|
||||
rules.value = next
|
||||
await persistRules()
|
||||
}
|
||||
|
||||
async function confirmDelete (index: number) {
|
||||
if (!rules.value[index]) return
|
||||
try {
|
||||
await $confirm($T('URL_REWRITE_DELETE_CONFIRM'), $T('TIPS_WARNING'), {
|
||||
type: 'warning',
|
||||
confirmButtonText: $T('CONFIRM'),
|
||||
cancelButtonText: $T('CANCEL')
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
rules.value.splice(index, 1)
|
||||
await persistRules()
|
||||
}
|
||||
|
||||
function openAddDialog () {
|
||||
editDialogMode.value = 'add'
|
||||
editRuleIndex.value = -1
|
||||
ruleForm.match = ''
|
||||
ruleForm.replace = ''
|
||||
ruleForm.enable = true
|
||||
ruleForm.global = false
|
||||
ruleForm.ignoreCase = false
|
||||
editDialogVisible.value = true
|
||||
}
|
||||
|
||||
function openEditDialog (rule: IUrlRewriteRule, index: number) {
|
||||
editDialogMode.value = 'edit'
|
||||
editRuleIndex.value = index
|
||||
ruleForm.match = rule.match
|
||||
ruleForm.replace = rule.replace
|
||||
ruleForm.enable = rule.enable
|
||||
ruleForm.global = rule.global
|
||||
ruleForm.ignoreCase = rule.ignoreCase
|
||||
editDialogVisible.value = true
|
||||
}
|
||||
|
||||
function cancelEditDialog () {
|
||||
editDialogVisible.value = false
|
||||
}
|
||||
|
||||
function buildRuleFlags (rule: Pick<IUrlRewriteRule, 'global' | 'ignoreCase'>) {
|
||||
return `${rule.global ? 'g' : ''}${rule.ignoreCase ? 'i' : ''}`
|
||||
}
|
||||
|
||||
function validateRuleOrThrow (rule: IUrlRewriteRule) {
|
||||
if (!rule.match.trim()) {
|
||||
throw new Error($T('URL_REWRITE_MATCH_REQUIRED'))
|
||||
}
|
||||
if (!rule.replace.trim()) {
|
||||
throw new Error($T('URL_REWRITE_REPLACE_REQUIRED'))
|
||||
}
|
||||
new RegExp(rule.match, buildRuleFlags(rule))
|
||||
}
|
||||
|
||||
function runPreview () {
|
||||
previewStatus.value = 'idle'
|
||||
previewMessage.value = ''
|
||||
previewOutputUrl.value = previewInputUrl.value
|
||||
|
||||
const url = previewInputUrl.value
|
||||
if (!url) {
|
||||
previewStatus.value = 'error'
|
||||
previewMessage.value = $T('URL_REWRITE_PREVIEW_INPUT_REQUIRED')
|
||||
return
|
||||
}
|
||||
|
||||
for (const [index, rule] of rules.value.entries()) {
|
||||
if (rule.enable === false) continue
|
||||
let regexp: RegExp
|
||||
try {
|
||||
regexp = new RegExp(rule.match, buildRuleFlags(rule))
|
||||
} catch (e) {
|
||||
previewStatus.value = 'error'
|
||||
previewMessage.value = `${$T('URL_REWRITE_PREVIEW_RULE_INVALID')} #${index + 1}: ${(e as Error).message}`
|
||||
return
|
||||
}
|
||||
|
||||
const matched = regexp.test(url)
|
||||
regexp.lastIndex = 0
|
||||
if (!matched) continue
|
||||
|
||||
const output = url.replace(regexp, rule.replace)
|
||||
previewOutputUrl.value = output
|
||||
previewStatus.value = 'matched'
|
||||
previewMessage.value = `${$T('URL_REWRITE_PREVIEW_MATCHED_RULE')} #${index + 1}`
|
||||
return
|
||||
}
|
||||
|
||||
previewStatus.value = 'noMatch'
|
||||
previewMessage.value = $T('URL_REWRITE_PREVIEW_NO_MATCH')
|
||||
}
|
||||
|
||||
async function confirmEditDialog () {
|
||||
const nextRule: IUrlRewriteRule = {
|
||||
match: ruleForm.match,
|
||||
replace: ruleForm.replace,
|
||||
enable: ruleForm.enable,
|
||||
global: ruleForm.global,
|
||||
ignoreCase: ruleForm.ignoreCase
|
||||
}
|
||||
|
||||
try {
|
||||
validateRuleOrThrow(nextRule)
|
||||
} catch (e) {
|
||||
$message.error((e as Error).message || $T('URL_REWRITE_INVALID_REGEX'))
|
||||
return
|
||||
}
|
||||
|
||||
if (editDialogMode.value === 'add') {
|
||||
rules.value.push(nextRule)
|
||||
} else if (editRuleIndex.value >= 0 && rules.value[editRuleIndex.value]) {
|
||||
rules.value.splice(editRuleIndex.value, 1, nextRule)
|
||||
}
|
||||
|
||||
await persistRules()
|
||||
editDialogVisible.value = false
|
||||
$message.success($T('TIPS_SET_SUCCEED'))
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'UrlRewritePage'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='stylus'>
|
||||
#url-rewrite-page
|
||||
.url-rewrite-list
|
||||
height 360px
|
||||
box-sizing border-box
|
||||
overflow-y auto
|
||||
overflow-x hidden
|
||||
width 100%
|
||||
.url-rewrite-panel
|
||||
background rgba(130, 130, 130, .12)
|
||||
border 1px solid darken(#eee, 50%)
|
||||
.url-rewrite-option-card
|
||||
background rgba(130, 130, 130, .12)
|
||||
border 1px solid rgba(255, 255, 255, .06)
|
||||
.url-rewrite-mono-box
|
||||
background rgba(130, 130, 130, .12)
|
||||
border 1px solid rgba(255, 255, 255, .06)
|
||||
.url-rewrite-table-border
|
||||
border-color darken(#eee, 50%)
|
||||
.el-checkbox__label
|
||||
color #aaa
|
||||
.el-table
|
||||
background-color: transparent
|
||||
color #ddd
|
||||
&::before
|
||||
background-color darken(#eee, 50%)
|
||||
thead
|
||||
color #bbb
|
||||
th,tr
|
||||
background-color: transparent
|
||||
&__body
|
||||
tr.el-table__row--striped
|
||||
td
|
||||
background transparent
|
||||
&--enable-row-hover
|
||||
.el-table__body
|
||||
tr:hover
|
||||
&>td
|
||||
background #333
|
||||
.el-button+.el-button
|
||||
margin-left 4px
|
||||
.el-button
|
||||
&.danger
|
||||
color: #F56C6C
|
||||
</style>
|
||||
@@ -14,6 +14,11 @@
|
||||
:button-label="$T('SETTINGS_CLICK_TO_SET')"
|
||||
@click="goShortCutPage"
|
||||
/>
|
||||
<ButtonFormItem
|
||||
:label="$T('SETTINGS_URL_REWRITE')"
|
||||
:button-label="$T('SETTINGS_CLICK_TO_SET')"
|
||||
@click="goUrlRewritePage"
|
||||
/>
|
||||
<ButtonFormItem
|
||||
:label="$T('SETTINGS_CUSTOM_LINK_FORMAT')"
|
||||
:button-label="$T('SETTINGS_CLICK_TO_SET')"
|
||||
@@ -72,7 +77,7 @@ import { reactive, ref } from 'vue'
|
||||
import { PICGO_OPEN_FILE } from '~/universal/events/constants'
|
||||
import LogSettingDialog from './LogSettingDialog.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { SHORTKEY_PAGE } from '@/router/config'
|
||||
import { SHORTKEY_PAGE, URL_REWRITE_PAGE } from '@/router/config'
|
||||
import { useVModel } from '@/hooks/useVModel'
|
||||
|
||||
const $router = useRouter()
|
||||
@@ -103,6 +108,12 @@ function goShortCutPage () {
|
||||
})
|
||||
}
|
||||
|
||||
function goUrlRewritePage () {
|
||||
$router.push({
|
||||
name: URL_REWRITE_PAGE
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
|
||||
@@ -8,5 +8,6 @@ export const PICBEDS_PAGE = 'PicbedsPage'
|
||||
export const SETTING_PAGE = 'SettingPage'
|
||||
export const PLUGIN_PAGE = 'PluginPage'
|
||||
export const SHORTKEY_PAGE = 'ShortkeyPage'
|
||||
export const URL_REWRITE_PAGE = 'UrlRewritePage'
|
||||
export const UPLOADER_CONFIG_PAGE = 'UploaderConfigPage'
|
||||
export const TOOLBOX_CONFIG_PAGE = 'ToolBoxPage'
|
||||
|
||||
@@ -57,6 +57,11 @@ export default createRouter({
|
||||
component: () => import(/* webpackChunkName: "ShortkeyPage" */ '@/pages/ShortKey.vue'),
|
||||
name: config.SHORTKEY_PAGE
|
||||
},
|
||||
{
|
||||
path: 'urlRewrite',
|
||||
component: () => import(/* webpackChunkName: "UrlRewritePage" */ '@/pages/UrlRewrite.vue'),
|
||||
name: config.URL_REWRITE_PAGE
|
||||
},
|
||||
{
|
||||
path: 'uploader-config-page/:type',
|
||||
component: () => import(/* webpackChunkName: "Other" */ '@/pages/UploaderConfigPage.vue'),
|
||||
|
||||
@@ -25,13 +25,14 @@ export const trimValues = (obj: IStringKeyMap) => {
|
||||
* get raw data from reactive or ref
|
||||
*/
|
||||
export const getRawData = (args: any): any => {
|
||||
if (args === null) return args
|
||||
if (Array.isArray(args)) {
|
||||
const data = args.map((item: any) => {
|
||||
if (isRef(item)) {
|
||||
return unref(item)
|
||||
return getRawData(unref(item))
|
||||
}
|
||||
if (isReactive(item)) {
|
||||
return toRaw(item)
|
||||
return getRawData(toRaw(item))
|
||||
}
|
||||
return getRawData(item)
|
||||
})
|
||||
@@ -42,9 +43,9 @@ export const getRawData = (args: any): any => {
|
||||
Object.keys(args).forEach(key => {
|
||||
const item = args[key]
|
||||
if (isRef(item)) {
|
||||
data[key] = unref(item)
|
||||
data[key] = getRawData(unref(item))
|
||||
} else if (isReactive(item)) {
|
||||
data[key] = toRaw(item)
|
||||
data[key] = getRawData(toRaw(item))
|
||||
} else {
|
||||
data[key] = getRawData(item)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export async function saveConfig (_config: IObj | string, value?: any) {
|
||||
let config
|
||||
if (typeof _config === 'string') {
|
||||
config = {
|
||||
[_config]: value
|
||||
[_config]: getRawData(value)
|
||||
}
|
||||
} else {
|
||||
config = getRawData(_config)
|
||||
|
||||
Vendored
+51
-5
@@ -63,6 +63,7 @@ interface ILocales {
|
||||
SETTINGS_CLICK_TO_SET: string
|
||||
SETTINGS_CLICK_TO_CHECK: string
|
||||
SETTINGS_SET_SHORTCUT: string
|
||||
SETTINGS_URL_REWRITE: string
|
||||
SETTINGS_CUSTOM_LINK_FORMAT: string
|
||||
SETTINGS_SET_PROXY_AND_MIRROR: string
|
||||
SETTINGS_SET_SERVER: string
|
||||
@@ -113,6 +114,43 @@ interface ILocales {
|
||||
SETTINGS_CHOOSE_LANGUAGE: string
|
||||
BUILTIN_CLIPBOARD_TIPS: string
|
||||
UPLOADER_CONFIG_NAME: string
|
||||
URL_REWRITE_HELP: string
|
||||
URL_REWRITE_ADD_RULE: string
|
||||
URL_REWRITE_EDIT_RULE: string
|
||||
URL_REWRITE_EMPTY: string
|
||||
URL_REWRITE_ORDER: string
|
||||
URL_REWRITE_MATCH: string
|
||||
URL_REWRITE_REPLACE: string
|
||||
URL_REWRITE_FLAGS: string
|
||||
URL_REWRITE_ENABLED: string
|
||||
URL_REWRITE_ACTIONS: string
|
||||
URL_REWRITE_MOVE_UP: string
|
||||
URL_REWRITE_MOVE_DOWN: string
|
||||
URL_REWRITE_EDIT: string
|
||||
URL_REWRITE_DELETE: string
|
||||
URL_REWRITE_DELETE_CONFIRM: string
|
||||
URL_REWRITE_MATCH_TIPS: string
|
||||
URL_REWRITE_MATCH_PLACEHOLDER: string
|
||||
URL_REWRITE_REPLACE_TIPS: string
|
||||
URL_REWRITE_REPLACE_PLACEHOLDER: string
|
||||
URL_REWRITE_OPTIONS: string
|
||||
URL_REWRITE_RULE_ENABLED: string
|
||||
URL_REWRITE_FLAG_GLOBAL_LABEL: string
|
||||
URL_REWRITE_FLAG_GLOBAL_DESC: string
|
||||
URL_REWRITE_FLAG_IGNORE_CASE_LABEL: string
|
||||
URL_REWRITE_FLAG_IGNORE_CASE_DESC: string
|
||||
URL_REWRITE_MATCH_REQUIRED: string
|
||||
URL_REWRITE_REPLACE_REQUIRED: string
|
||||
URL_REWRITE_INVALID_REGEX: string
|
||||
URL_REWRITE_PREVIEW_TITLE: string
|
||||
URL_REWRITE_PREVIEW_TIPS: string
|
||||
URL_REWRITE_PREVIEW_PLACEHOLDER: string
|
||||
URL_REWRITE_PREVIEW_RUN: string
|
||||
URL_REWRITE_PREVIEW_OUTPUT: string
|
||||
URL_REWRITE_PREVIEW_INPUT_REQUIRED: string
|
||||
URL_REWRITE_PREVIEW_RULE_INVALID: string
|
||||
URL_REWRITE_PREVIEW_MATCHED_RULE: string
|
||||
URL_REWRITE_PREVIEW_NO_MATCH: string
|
||||
UPLOADER_CONFIG_PLACEHOLDER: string
|
||||
SELECTED_SETTING_HINT: string
|
||||
SETTINGS_ENCODE_OUTPUT_URL: string
|
||||
@@ -134,11 +172,19 @@ interface ILocales {
|
||||
SHORTCUT_DISABLE: string
|
||||
SHORTCUT_EDIT: string
|
||||
SHORTCUT_CHANGE_UPLOAD: string
|
||||
CHANGE_IMAGE_URL_HOST: string
|
||||
NEW_IMAGE_URL_HOST: string
|
||||
SELECTED_IMAGE_URL_HOST: string
|
||||
CHANGE_IMAGE_URL_HOST_RESULT: string
|
||||
CHANGE_IMAGE_URL_HOST_WARN: string
|
||||
GALLERY_URL_REWRITE_TITLE: string
|
||||
GALLERY_URL_REWRITE_RESULT_TITLE: string
|
||||
GALLERY_URL_REWRITE_WARN_NO_SELECTION: string
|
||||
GALLERY_URL_REWRITE_APPLY_GLOBAL_RULES: string
|
||||
GALLERY_URL_REWRITE_GLOBAL_RULES_COUNT: string
|
||||
GALLERY_URL_REWRITE_TEMP_RULE_TIPS: string
|
||||
GALLERY_URL_REWRITE_TEMP_RULE_REQUIRED: string
|
||||
GALLERY_URL_REWRITE_NO_RULES_TO_APPLY: string
|
||||
GALLERY_URL_REWRITE_SAVE_TEMP_RULE_PROMPT: string
|
||||
GALLERY_URL_REWRITE_APPLY_AND_SAVE: string
|
||||
GALLERY_URL_REWRITE_APPLY_ONLY: string
|
||||
GALLERY_URL_REWRITE_NO_CHANGES: string
|
||||
GALLERY_URL_REWRITE_EMPTY_RESULT_WARN: string
|
||||
WAIT_TO_UPLOAD: string
|
||||
ALREADY_UPLOAD: string
|
||||
PICTURE_UPLOAD: string
|
||||
|
||||
Vendored
+1
@@ -55,6 +55,7 @@ interface ImgInfo {
|
||||
imgUrl?: string
|
||||
id?: string
|
||||
type?: string
|
||||
originImgUrl?: string
|
||||
[propName: string]: any
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user