mirror of https://github.com/halo-dev/halo
feat: add i18n supports for console (#3506)
#### What type of PR is this? /kind feature #### What this PR does / why we need it: 为 Console 端添加多语言的支持,并默认提供简体中文和英文的语言包。 todolist: - [x] 完善 Console 的文字语言包翻译。 - [ ] ~~为后端提供的部分数据支持翻译,比如系统设置的表单定义。(实现方式待讨论,这个 PR 先不支持)~~ - [x] 提供语言设置。 #### Which issue(s) this PR fixes: Fixes #3346 #### Special notes for your reviewer: 测试方式: 1. 检查各个页面的文字显示是否正常。 2. 测试中英文环境中是否使用了对应的语言包。 #### Does this PR introduce a user-facing change? ```release-note Console 端支持多语言界面 ```pull/3548/head^2
parent
c400c85922
commit
b63d2b882c
|
@ -1,2 +1 @@
|
|||
# 排除 pnpm-lock.yaml,防止被 prettier 影响导致不必要的 diff
|
||||
pnpm-lock.yaml
|
||||
|
|
|
@ -68,6 +68,7 @@
|
|||
"fastq": "^1.15.0",
|
||||
"floating-vue": "2.0.0-beta.20",
|
||||
"fuse.js": "^6.6.2",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
|
@ -83,13 +84,13 @@
|
|||
"vue-grid-layout": "3.0.0-beta1",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-router": "^4.1.6",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"jsencrypt": "^3.3.2"
|
||||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.25.2",
|
||||
"@iconify-json/mdi": "^1.1.36",
|
||||
"@iconify-json/vscode-icons": "^1.1.16",
|
||||
"@intlify/unplugin-vue-i18n": "^0.9.1",
|
||||
"@rushstack/eslint-patch": "^1.2.0",
|
||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||
"@tailwindcss/container-queries": "^0.1.0",
|
||||
|
|
|
@ -9,12 +9,16 @@ const props = withDefaults(
|
|||
size?: number;
|
||||
total?: number;
|
||||
sizeOptions?: number[];
|
||||
pageLabel?: string;
|
||||
sizeLabel?: string;
|
||||
}>(),
|
||||
{
|
||||
page: 1,
|
||||
size: 10,
|
||||
total: 0,
|
||||
sizeOptions: () => [10],
|
||||
pageLabel: "页",
|
||||
sizeLabel: "条 / 页",
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -115,7 +119,7 @@ const {
|
|||
{{ i }} / {{ pageCount }}
|
||||
</option>
|
||||
</select>
|
||||
<span class="text-sm text-gray-500">页</span>
|
||||
<span class="text-sm text-gray-500">{{ pageLabel }}</span>
|
||||
</div>
|
||||
<div class="inline-flex items-center gap-2">
|
||||
<select
|
||||
|
@ -130,7 +134,7 @@ const {
|
|||
{{ sizeOption }}
|
||||
</option>
|
||||
</select>
|
||||
<span class="text-sm text-gray-500">条 / 页</span>
|
||||
<span class="text-sm text-gray-500">{{ sizeLabel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -19,6 +19,7 @@ importers:
|
|||
'@halo-dev/richtext-editor': 0.0.0-alpha.19
|
||||
'@iconify-json/mdi': ^1.1.36
|
||||
'@iconify-json/vscode-icons': ^1.1.16
|
||||
'@intlify/unplugin-vue-i18n': ^0.9.1
|
||||
'@rushstack/eslint-patch': ^1.2.0
|
||||
'@tailwindcss/aspect-ratio': ^0.4.2
|
||||
'@tailwindcss/container-queries': ^0.1.0
|
||||
|
@ -161,6 +162,7 @@ importers:
|
|||
'@changesets/cli': 2.25.2
|
||||
'@iconify-json/mdi': 1.1.36
|
||||
'@iconify-json/vscode-icons': 1.1.16
|
||||
'@intlify/unplugin-vue-i18n': 0.9.1_2zqjidl67vtf3eplgf74ci35dq
|
||||
'@rushstack/eslint-patch': 1.2.0
|
||||
'@tailwindcss/aspect-ratio': 0.4.2_tailwindcss@3.2.4
|
||||
'@tailwindcss/container-queries': 0.1.0_tailwindcss@3.2.4
|
||||
|
@ -367,6 +369,16 @@ packages:
|
|||
jsesc: 2.5.2
|
||||
dev: true
|
||||
|
||||
/@babel/generator/7.21.1:
|
||||
resolution: {integrity: sha512-1lT45bAYlQhFn/BHivJs43AiW2rg3/UbLyShGfF3C0KmHvO5fSghWd5kBJy30kpRRucGzXStvnnCFniCR2kXAA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.21.2
|
||||
'@jridgewell/gen-mapping': 0.3.2
|
||||
'@jridgewell/trace-mapping': 0.3.17
|
||||
jsesc: 2.5.2
|
||||
dev: true
|
||||
|
||||
/@babel/helper-annotate-as-pure/7.18.6:
|
||||
resolution: {integrity: sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
@ -379,7 +391,7 @@ packages:
|
|||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/helper-explode-assignable-expression': 7.18.6
|
||||
'@babel/types': 7.20.7
|
||||
'@babel/types': 7.21.2
|
||||
dev: true
|
||||
|
||||
/@babel/helper-compilation-targets/7.20.7_@babel+core@7.20.12:
|
||||
|
@ -450,7 +462,7 @@ packages:
|
|||
resolution: {integrity: sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.20.7
|
||||
'@babel/types': 7.21.2
|
||||
dev: true
|
||||
|
||||
/@babel/helper-function-name/7.19.0:
|
||||
|
@ -461,6 +473,14 @@ packages:
|
|||
'@babel/types': 7.20.2
|
||||
dev: true
|
||||
|
||||
/@babel/helper-function-name/7.21.0:
|
||||
resolution: {integrity: sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/template': 7.20.7
|
||||
'@babel/types': 7.21.2
|
||||
dev: true
|
||||
|
||||
/@babel/helper-hoist-variables/7.18.6:
|
||||
resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
@ -520,7 +540,7 @@ packages:
|
|||
'@babel/helper-annotate-as-pure': 7.18.6
|
||||
'@babel/helper-environment-visitor': 7.18.9
|
||||
'@babel/helper-wrap-function': 7.19.0
|
||||
'@babel/types': 7.20.7
|
||||
'@babel/types': 7.21.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
@ -549,7 +569,7 @@ packages:
|
|||
resolution: {integrity: sha512-imytd2gHi3cJPsybLRbmFrF7u5BIEuI2cNheyKi3/iOBC63kNn3q8Crn2xVuESli0aM4KYsyEqKyS7lFL8YVtw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.20.7
|
||||
'@babel/types': 7.21.2
|
||||
dev: true
|
||||
|
||||
/@babel/helper-split-export-declaration/7.18.6:
|
||||
|
@ -576,10 +596,10 @@ packages:
|
|||
resolution: {integrity: sha512-txX8aN8CZyYGTwcLhlk87KRqncAzhh5TpQamZUa0/u3an36NtDpUP6bQgBCBcLeBs09R/OwQu3OjK0k/HwfNDg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/helper-function-name': 7.19.0
|
||||
'@babel/helper-function-name': 7.21.0
|
||||
'@babel/template': 7.20.7
|
||||
'@babel/traverse': 7.20.12
|
||||
'@babel/types': 7.20.7
|
||||
'@babel/traverse': 7.21.2
|
||||
'@babel/types': 7.21.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
@ -618,6 +638,14 @@ packages:
|
|||
dependencies:
|
||||
'@babel/types': 7.20.7
|
||||
|
||||
/@babel/parser/7.21.2:
|
||||
resolution: {integrity: sha512-URpaIJQwEkEC2T9Kn+Ai6Xe/02iNaVCuT/PtoRz3GPVJVDpPd7mLo+VddTbhCRU9TXqW5mSrQfXZyi8kDKOVpQ==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
'@babel/types': 7.20.7
|
||||
dev: true
|
||||
|
||||
/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/7.18.6_@babel+core@7.20.12:
|
||||
resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
@ -1037,7 +1065,7 @@ packages:
|
|||
'@babel/helper-annotate-as-pure': 7.18.6
|
||||
'@babel/helper-compilation-targets': 7.20.7_@babel+core@7.20.12
|
||||
'@babel/helper-environment-visitor': 7.18.9
|
||||
'@babel/helper-function-name': 7.19.0
|
||||
'@babel/helper-function-name': 7.21.0
|
||||
'@babel/helper-optimise-call-expression': 7.18.6
|
||||
'@babel/helper-plugin-utils': 7.20.2
|
||||
'@babel/helper-replace-supers': 7.19.1
|
||||
|
@ -1117,7 +1145,7 @@ packages:
|
|||
dependencies:
|
||||
'@babel/core': 7.20.12
|
||||
'@babel/helper-compilation-targets': 7.20.7_@babel+core@7.20.12
|
||||
'@babel/helper-function-name': 7.19.0
|
||||
'@babel/helper-function-name': 7.21.0
|
||||
'@babel/helper-plugin-utils': 7.20.2
|
||||
dev: true
|
||||
|
||||
|
@ -1436,7 +1464,7 @@ packages:
|
|||
'@babel/plugin-transform-unicode-escapes': 7.18.10_@babel+core@7.20.12
|
||||
'@babel/plugin-transform-unicode-regex': 7.18.6_@babel+core@7.20.12
|
||||
'@babel/preset-modules': 0.1.5_@babel+core@7.20.12
|
||||
'@babel/types': 7.20.7
|
||||
'@babel/types': 7.21.2
|
||||
babel-plugin-polyfill-corejs2: 0.3.3_@babel+core@7.20.12
|
||||
babel-plugin-polyfill-corejs3: 0.6.0_@babel+core@7.20.12
|
||||
babel-plugin-polyfill-regenerator: 0.4.1_@babel+core@7.20.12
|
||||
|
@ -1455,7 +1483,7 @@ packages:
|
|||
'@babel/helper-plugin-utils': 7.20.2
|
||||
'@babel/plugin-proposal-unicode-property-regex': 7.18.6_@babel+core@7.20.12
|
||||
'@babel/plugin-transform-dotall-regex': 7.18.6_@babel+core@7.20.12
|
||||
'@babel/types': 7.20.7
|
||||
'@babel/types': 7.21.2
|
||||
esutils: 2.0.3
|
||||
dev: true
|
||||
|
||||
|
@ -1525,6 +1553,24 @@ packages:
|
|||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@babel/traverse/7.21.2:
|
||||
resolution: {integrity: sha512-ts5FFU/dSUPS13tv8XiEObDu9K+iagEKME9kAbaP7r0Y9KtZJZ+NGndDvWoRAYNpeWafbpFeki3q9QoMD6gxyw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.18.6
|
||||
'@babel/generator': 7.21.1
|
||||
'@babel/helper-environment-visitor': 7.18.9
|
||||
'@babel/helper-function-name': 7.21.0
|
||||
'@babel/helper-hoist-variables': 7.18.6
|
||||
'@babel/helper-split-export-declaration': 7.18.6
|
||||
'@babel/parser': 7.21.2
|
||||
'@babel/types': 7.21.2
|
||||
debug: 4.3.4
|
||||
globals: 11.12.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@babel/types/7.20.2:
|
||||
resolution: {integrity: sha512-FnnvsNWgZCr232sqtXggapvlkk/tuwR/qhGzcmxI0GXLCjmPYQPzio2FbdlWuY6y1sHFfQKk+rRbUZ9VStQMog==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
@ -1541,6 +1587,15 @@ packages:
|
|||
'@babel/helper-validator-identifier': 7.19.1
|
||||
to-fast-properties: 2.0.0
|
||||
|
||||
/@babel/types/7.21.2:
|
||||
resolution: {integrity: sha512-3wRZSs7jiFaB8AjxiiD+VqN5DTG2iRvJGQ+qYFrs/654lg6kGTQWIOFjlBo5RaXuAZjBmP3+OQH4dmhqiiyYxw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/helper-string-parser': 7.19.4
|
||||
'@babel/helper-validator-identifier': 7.19.1
|
||||
to-fast-properties: 2.0.0
|
||||
dev: true
|
||||
|
||||
/@bcoe/v8-coverage/0.2.3:
|
||||
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
|
||||
dev: true
|
||||
|
@ -2576,6 +2631,30 @@ packages:
|
|||
resolution: {integrity: sha512-sZAW08CkqgvqRjUIaLRjScjObcCzN9D75yekLA21EClYAZIhi4A+GEt2z/WqOCOksTaEPLYmQyhkpXcboc0LhQ==}
|
||||
dev: false
|
||||
|
||||
/@intlify/bundle-utils/5.0.1_vue-i18n@9.2.2:
|
||||
resolution: {integrity: sha512-qF6reZHDm+h7jUId2npzwNZYCvrUcr0bAYnJXgiShKBxTFpxOq7nrqy7UrRWj8M2w4GTCJczeQZmQGFxc/GdFA==}
|
||||
engines: {node: '>= 12'}
|
||||
peerDependencies:
|
||||
petite-vue-i18n: '*'
|
||||
vue-i18n: '*'
|
||||
peerDependenciesMeta:
|
||||
petite-vue-i18n:
|
||||
optional: true
|
||||
vue-i18n:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/parser': 7.21.2
|
||||
'@babel/traverse': 7.21.2
|
||||
'@intlify/message-compiler': 9.3.0-beta.16
|
||||
'@intlify/shared': 9.3.0-beta.16
|
||||
jsonc-eslint-parser: 1.4.1
|
||||
source-map: 0.6.1
|
||||
vue-i18n: 9.2.2_vue@3.2.45
|
||||
yaml-eslint-parser: 0.3.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@intlify/core-base/9.2.2:
|
||||
resolution: {integrity: sha512-JjUpQtNfn+joMbrXvpR4hTF8iJQ2sEFzzK3KIESOx+f+uwIjgw20igOyaIdhfsVVBCds8ZM64MoeNSx+PHQMkA==}
|
||||
engines: {node: '>= 14'}
|
||||
|
@ -2584,14 +2663,12 @@ packages:
|
|||
'@intlify/message-compiler': 9.2.2
|
||||
'@intlify/shared': 9.2.2
|
||||
'@intlify/vue-devtools': 9.2.2
|
||||
dev: false
|
||||
|
||||
/@intlify/devtools-if/9.2.2:
|
||||
resolution: {integrity: sha512-4ttr/FNO29w+kBbU7HZ/U0Lzuh2cRDhP8UlWOtV9ERcjHzuyXVZmjyleESK6eVP60tGC9QtQW9yZE+JeRhDHkg==}
|
||||
engines: {node: '>= 14'}
|
||||
dependencies:
|
||||
'@intlify/shared': 9.2.2
|
||||
dev: false
|
||||
|
||||
/@intlify/message-compiler/9.2.2:
|
||||
resolution: {integrity: sha512-IUrQW7byAKN2fMBe8z6sK6riG1pue95e5jfokn8hA5Q3Bqy4MBJ5lJAofUsawQJYHeoPJ7svMDyBaVJ4d0GTtA==}
|
||||
|
@ -2599,12 +2676,56 @@ packages:
|
|||
dependencies:
|
||||
'@intlify/shared': 9.2.2
|
||||
source-map: 0.6.1
|
||||
dev: false
|
||||
|
||||
/@intlify/message-compiler/9.3.0-beta.16:
|
||||
resolution: {integrity: sha512-CGQI3xRcs1ET75eDQ0DUy3MRYOqTauRIIgaMoISKiF83gqRWg93FqN8lGMKcpBqaF4tI0JhsfosCaGiBL9+dnw==}
|
||||
engines: {node: '>= 14'}
|
||||
dependencies:
|
||||
'@intlify/shared': 9.3.0-beta.16
|
||||
source-map: 0.6.1
|
||||
dev: true
|
||||
|
||||
/@intlify/shared/9.2.2:
|
||||
resolution: {integrity: sha512-wRwTpsslgZS5HNyM7uDQYZtxnbI12aGiBZURX3BTR9RFIKKRWpllTsgzHWvj3HKm3Y2Sh5LPC1r0PDCKEhVn9Q==}
|
||||
engines: {node: '>= 14'}
|
||||
dev: false
|
||||
|
||||
/@intlify/shared/9.3.0-beta.16:
|
||||
resolution: {integrity: sha512-kXbm4svALe3lX+EjdJxfnabOphqS4yQ1Ge/iIlR8tvUiYRCoNz3hig1M4336iY++Dfx5ytEQJPNjIcknNIuvig==}
|
||||
engines: {node: '>= 14'}
|
||||
dev: true
|
||||
|
||||
/@intlify/unplugin-vue-i18n/0.9.1_2zqjidl67vtf3eplgf74ci35dq:
|
||||
resolution: {integrity: sha512-7HLs5VjzK702Bqofrq9aA2yujcg8EJEtikspr0Zx6wXuI5rYq5VnejUWns+ZTdRjqfbd6DkFGNBooOLVfkezBQ==}
|
||||
engines: {node: '>= 14.16'}
|
||||
peerDependencies:
|
||||
petite-vue-i18n: '*'
|
||||
vue-i18n: '*'
|
||||
vue-i18n-bridge: '*'
|
||||
peerDependenciesMeta:
|
||||
petite-vue-i18n:
|
||||
optional: true
|
||||
vue-i18n:
|
||||
optional: true
|
||||
vue-i18n-bridge:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@intlify/bundle-utils': 5.0.1_vue-i18n@9.2.2
|
||||
'@intlify/shared': 9.3.0-beta.16
|
||||
'@rollup/pluginutils': 5.0.2_rollup@2.79.1
|
||||
'@vue/compiler-sfc': 3.2.47
|
||||
debug: 4.3.4
|
||||
fast-glob: 3.2.12
|
||||
js-yaml: 4.1.0
|
||||
json5: 2.2.3
|
||||
pathe: 1.1.0
|
||||
picocolors: 1.0.0
|
||||
source-map: 0.6.1
|
||||
unplugin: 1.3.0
|
||||
vue-i18n: 9.2.2_vue@3.2.45
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@intlify/vue-devtools/9.2.2:
|
||||
resolution: {integrity: sha512-+dUyqyCHWHb/UcvY1MlIpO87munedm3Gn6E9WWYdWrMuYLcoIoOEVDWSS8xSwtlPU+kA+MEQTP6Q1iI/ocusJg==}
|
||||
|
@ -2612,7 +2733,6 @@ packages:
|
|||
dependencies:
|
||||
'@intlify/core-base': 9.2.2
|
||||
'@intlify/shared': 9.2.2
|
||||
dev: false
|
||||
|
||||
/@istanbuljs/schema/0.1.3:
|
||||
resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
|
||||
|
@ -2664,6 +2784,13 @@ packages:
|
|||
'@jridgewell/sourcemap-codec': 1.4.14
|
||||
dev: true
|
||||
|
||||
/@jridgewell/trace-mapping/0.3.17:
|
||||
resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==}
|
||||
dependencies:
|
||||
'@jridgewell/resolve-uri': 3.1.0
|
||||
'@jridgewell/sourcemap-codec': 1.4.14
|
||||
dev: true
|
||||
|
||||
/@jsdevtools/ez-spawn/3.0.4:
|
||||
resolution: {integrity: sha512-f5DRIOZf7wxogefH03RjMPMdBF7ADTWUMoOs9kaJo06EfwF+aFhMZMDZxHg/Xe12hptN9xoZjGso2fdjapBRIA==}
|
||||
engines: {node: '>=10'}
|
||||
|
@ -3038,6 +3165,21 @@ packages:
|
|||
picomatch: 2.3.1
|
||||
dev: true
|
||||
|
||||
/@rollup/pluginutils/5.0.2_rollup@2.79.1:
|
||||
resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
rollup: ^1.20.0||^2.0.0||^3.0.0
|
||||
peerDependenciesMeta:
|
||||
rollup:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@types/estree': 1.0.0
|
||||
estree-walker: 2.0.2
|
||||
picomatch: 2.3.1
|
||||
rollup: 2.79.1
|
||||
dev: true
|
||||
|
||||
/@rushstack/eslint-patch/1.2.0:
|
||||
resolution: {integrity: sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==}
|
||||
dev: true
|
||||
|
@ -4178,12 +4320,28 @@ packages:
|
|||
estree-walker: 2.0.2
|
||||
source-map: 0.6.1
|
||||
|
||||
/@vue/compiler-core/3.2.47:
|
||||
resolution: {integrity: sha512-p4D7FDnQb7+YJmO2iPEv0SQNeNzcbHdGByJDsT4lynf63AFkOTFN07HsiRSvjGo0QrxR/o3d0hUyNCUnBU2Tig==}
|
||||
dependencies:
|
||||
'@babel/parser': 7.20.7
|
||||
'@vue/shared': 3.2.47
|
||||
estree-walker: 2.0.2
|
||||
source-map: 0.6.1
|
||||
dev: true
|
||||
|
||||
/@vue/compiler-dom/3.2.45:
|
||||
resolution: {integrity: sha512-tyYeUEuKqqZO137WrZkpwfPCdiiIeXYCcJ8L4gWz9vqaxzIQRccTSwSWZ/Axx5YR2z+LvpUbmPNXxuBU45lyRw==}
|
||||
dependencies:
|
||||
'@vue/compiler-core': 3.2.45
|
||||
'@vue/shared': 3.2.45
|
||||
|
||||
/@vue/compiler-dom/3.2.47:
|
||||
resolution: {integrity: sha512-dBBnEHEPoftUiS03a4ggEig74J2YBZ2UIeyfpcRM2tavgMWo4bsEfgCGsu+uJIL/vax9S+JztH8NmQerUo7shQ==}
|
||||
dependencies:
|
||||
'@vue/compiler-core': 3.2.47
|
||||
'@vue/shared': 3.2.47
|
||||
dev: true
|
||||
|
||||
/@vue/compiler-sfc/3.2.45:
|
||||
resolution: {integrity: sha512-1jXDuWah1ggsnSAOGsec8cFjT/K6TMZ0sPL3o3d84Ft2AYZi2jWJgRMjw4iaK0rBfA89L5gw427H4n1RZQBu6Q==}
|
||||
dependencies:
|
||||
|
@ -4198,15 +4356,36 @@ packages:
|
|||
postcss: 8.4.19
|
||||
source-map: 0.6.1
|
||||
|
||||
/@vue/compiler-sfc/3.2.47:
|
||||
resolution: {integrity: sha512-rog05W+2IFfxjMcFw10tM9+f7i/+FFpZJJ5XHX72NP9eC2uRD+42M3pYcQqDXVYoj74kHMSEdQ/WmCjt8JFksQ==}
|
||||
dependencies:
|
||||
'@babel/parser': 7.20.7
|
||||
'@vue/compiler-core': 3.2.47
|
||||
'@vue/compiler-dom': 3.2.47
|
||||
'@vue/compiler-ssr': 3.2.47
|
||||
'@vue/reactivity-transform': 3.2.47
|
||||
'@vue/shared': 3.2.47
|
||||
estree-walker: 2.0.2
|
||||
magic-string: 0.25.9
|
||||
postcss: 8.4.21
|
||||
source-map: 0.6.1
|
||||
dev: true
|
||||
|
||||
/@vue/compiler-ssr/3.2.45:
|
||||
resolution: {integrity: sha512-6BRaggEGqhWht3lt24CrIbQSRD5O07MTmd+LjAn5fJj568+R9eUD2F7wMQJjX859seSlrYog7sUtrZSd7feqrQ==}
|
||||
dependencies:
|
||||
'@vue/compiler-dom': 3.2.45
|
||||
'@vue/shared': 3.2.45
|
||||
|
||||
/@vue/compiler-ssr/3.2.47:
|
||||
resolution: {integrity: sha512-wVXC+gszhulcMD8wpxMsqSOpvDZ6xKXSVWkf50Guf/S+28hTAXPDYRTbLQ3EDkOP5Xz/+SY37YiwDquKbJOgZw==}
|
||||
dependencies:
|
||||
'@vue/compiler-dom': 3.2.47
|
||||
'@vue/shared': 3.2.47
|
||||
dev: true
|
||||
|
||||
/@vue/devtools-api/6.4.5:
|
||||
resolution: {integrity: sha512-JD5fcdIuFxU4fQyXUu3w2KpAJHzTVdN+p4iOX2lMWSHMOoQdMAcpFLZzm9Z/2nmsoZ1a96QEhZ26e50xLBsgOQ==}
|
||||
dev: false
|
||||
|
||||
/@vue/eslint-config-prettier/7.0.0_5qrnzwqb344w6up62gv3safeoi:
|
||||
resolution: {integrity: sha512-/CTc6ML3Wta1tCe1gUeO0EYnVXfo3nJXsIhZ8WJr3sov+cGASr6yuiibJTL6lmIBm7GobopToOuB3B6AWyV0Iw==}
|
||||
|
@ -4250,6 +4429,16 @@ packages:
|
|||
estree-walker: 2.0.2
|
||||
magic-string: 0.25.9
|
||||
|
||||
/@vue/reactivity-transform/3.2.47:
|
||||
resolution: {integrity: sha512-m8lGXw8rdnPVVIdIFhf0LeQ/ixyHkH5plYuS83yop5n7ggVJU+z5v0zecwEnX7fa7HNLBhh2qngJJkxpwEEmYA==}
|
||||
dependencies:
|
||||
'@babel/parser': 7.20.7
|
||||
'@vue/compiler-core': 3.2.47
|
||||
'@vue/shared': 3.2.47
|
||||
estree-walker: 2.0.2
|
||||
magic-string: 0.25.9
|
||||
dev: true
|
||||
|
||||
/@vue/reactivity/3.2.45:
|
||||
resolution: {integrity: sha512-PRvhCcQcyEVohW0P8iQ7HDcIOXRjZfAsOds3N99X/Dzewy8TVhTCT4uXpAHfoKjVTJRA0O0K+6QNkDIZAxNi3A==}
|
||||
dependencies:
|
||||
|
@ -4280,6 +4469,10 @@ packages:
|
|||
/@vue/shared/3.2.45:
|
||||
resolution: {integrity: sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg==}
|
||||
|
||||
/@vue/shared/3.2.47:
|
||||
resolution: {integrity: sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ==}
|
||||
dev: true
|
||||
|
||||
/@vue/test-utils/2.2.4_vue@3.2.45:
|
||||
resolution: {integrity: sha512-1JjLduJ84bFcuCt/1YLTNyktYeUHS/zA0u8iTmF6w6ul1K/nSvyKu/MC47YjdpZ4lI/hn7FH31B22kfz62e9wA==}
|
||||
peerDependencies:
|
||||
|
@ -4359,6 +4552,14 @@ packages:
|
|||
acorn-walk: 8.2.0
|
||||
dev: true
|
||||
|
||||
/acorn-jsx/5.3.2_acorn@7.4.1:
|
||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||
peerDependencies:
|
||||
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
dependencies:
|
||||
acorn: 7.4.1
|
||||
dev: true
|
||||
|
||||
/acorn-jsx/5.3.2_acorn@8.8.0:
|
||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||
peerDependencies:
|
||||
|
@ -4400,6 +4601,12 @@ packages:
|
|||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/acorn/8.8.2:
|
||||
resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/agent-base/6.0.2:
|
||||
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
|
||||
engines: {node: '>= 6.0.0'}
|
||||
|
@ -6276,6 +6483,13 @@ packages:
|
|||
estraverse: 5.3.0
|
||||
dev: true
|
||||
|
||||
/eslint-utils/2.1.0:
|
||||
resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==}
|
||||
engines: {node: '>=6'}
|
||||
dependencies:
|
||||
eslint-visitor-keys: 1.3.0
|
||||
dev: true
|
||||
|
||||
/eslint-utils/3.0.0_eslint@8.28.0:
|
||||
resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==}
|
||||
engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0}
|
||||
|
@ -6286,6 +6500,11 @@ packages:
|
|||
eslint-visitor-keys: 2.1.0
|
||||
dev: true
|
||||
|
||||
/eslint-visitor-keys/1.3.0:
|
||||
resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==}
|
||||
engines: {node: '>=4'}
|
||||
dev: true
|
||||
|
||||
/eslint-visitor-keys/2.1.0:
|
||||
resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==}
|
||||
engines: {node: '>=10'}
|
||||
|
@ -6344,6 +6563,15 @@ packages:
|
|||
- supports-color
|
||||
dev: true
|
||||
|
||||
/espree/6.2.1:
|
||||
resolution: {integrity: sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
dependencies:
|
||||
acorn: 7.4.1
|
||||
acorn-jsx: 5.3.2_acorn@7.4.1
|
||||
eslint-visitor-keys: 1.3.0
|
||||
dev: true
|
||||
|
||||
/espree/9.4.0:
|
||||
resolution: {integrity: sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
@ -7694,6 +7922,17 @@ packages:
|
|||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/jsonc-eslint-parser/1.4.1:
|
||||
resolution: {integrity: sha512-hXBrvsR1rdjmB2kQmUjf1rEIa+TqHBGMge8pwi++C+Si1ad7EjZrJcpgwym+QGK/pqTx+K7keFAtLlVNdLRJOg==}
|
||||
engines: {node: '>=8.10.0'}
|
||||
dependencies:
|
||||
acorn: 7.4.1
|
||||
eslint-utils: 2.1.0
|
||||
eslint-visitor-keys: 1.3.0
|
||||
espree: 6.2.1
|
||||
semver: 6.3.0
|
||||
dev: true
|
||||
|
||||
/jsonc-parser/3.2.0:
|
||||
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
|
||||
dev: true
|
||||
|
@ -8580,6 +8819,10 @@ packages:
|
|||
resolution: {integrity: sha512-c71n61F1skhj/jzZe+fWE9XDoTYjWbUwIKVwFftZ5IOgiX44BVkTkD+/803YDgR50tqeO4eXWxLyVHBLWQAD1g==}
|
||||
dev: true
|
||||
|
||||
/pathe/1.1.0:
|
||||
resolution: {integrity: sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==}
|
||||
dev: true
|
||||
|
||||
/pathval/1.1.1:
|
||||
resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
|
||||
dev: true
|
||||
|
@ -10304,6 +10547,15 @@ packages:
|
|||
webpack-virtual-modules: 0.4.6
|
||||
dev: true
|
||||
|
||||
/unplugin/1.3.0:
|
||||
resolution: {integrity: sha512-l4Udjxg2+vCuKRgIA2T8fHd7UwKWaLizh7t+3C72zjnN0+ZS+odzATFenymOUgcGqG1dkCSYE34h9wBbMXrKrA==}
|
||||
dependencies:
|
||||
acorn: 8.8.2
|
||||
chokidar: 3.5.3
|
||||
webpack-sources: 3.2.3
|
||||
webpack-virtual-modules: 0.5.0
|
||||
dev: true
|
||||
|
||||
/untildify/4.0.0:
|
||||
resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==}
|
||||
engines: {node: '>=8'}
|
||||
|
@ -10859,7 +11111,6 @@ packages:
|
|||
'@intlify/vue-devtools': 9.2.2
|
||||
'@vue/devtools-api': 6.4.5
|
||||
vue: 3.2.45
|
||||
dev: false
|
||||
|
||||
/vue-resize/2.0.0-alpha.1_vue@3.2.45:
|
||||
resolution: {integrity: sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==}
|
||||
|
@ -10966,6 +11217,10 @@ packages:
|
|||
resolution: {integrity: sha512-5tyDlKLqPfMqjT3Q9TAqf2YqjwmnUleZwzJi1A5qXnlBCdj2AtOJ6wAWdglTIDOPgOiOrXeBeFcsQ8+aGQ6QbA==}
|
||||
dev: true
|
||||
|
||||
/webpack-virtual-modules/0.5.0:
|
||||
resolution: {integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==}
|
||||
dev: true
|
||||
|
||||
/whatwg-encoding/2.0.0:
|
||||
resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -11260,6 +11515,14 @@ packages:
|
|||
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
|
||||
dev: true
|
||||
|
||||
/yaml-eslint-parser/0.3.2:
|
||||
resolution: {integrity: sha512-32kYO6kJUuZzqte82t4M/gB6/+11WAuHiEnK7FreMo20xsCKPeFH5tDBU7iWxR7zeJpNnMXfJyXwne48D0hGrg==}
|
||||
dependencies:
|
||||
eslint-visitor-keys: 1.3.0
|
||||
lodash: 4.17.21
|
||||
yaml: 1.10.2
|
||||
dev: true
|
||||
|
||||
/yaml/1.10.2:
|
||||
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
|
||||
engines: {node: '>= 6'}
|
||||
|
|
|
@ -6,6 +6,9 @@ import { useFavicon } from "@vueuse/core";
|
|||
import { useSystemConfigMapStore } from "./stores/system-configmap";
|
||||
import { storeToRefs } from "pinia";
|
||||
import axios from "axios";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { configMap } = storeToRefs(useSystemConfigMapStore());
|
||||
|
||||
|
@ -18,7 +21,7 @@ watch(
|
|||
() => {
|
||||
const { title: routeTitle } = route.meta;
|
||||
if (routeTitle) {
|
||||
title.value = `${routeTitle} - ${AppName}`;
|
||||
title.value = `${t(routeTitle)} - ${AppName}`;
|
||||
return;
|
||||
}
|
||||
title.value = AppName;
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
import { VButton } from "@halo-dev/components";
|
||||
import { useMagicKeys } from "@vueuse/core";
|
||||
import { computed, useAttrs, watchEffect } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -21,7 +24,10 @@ const isMac = /macintosh|mac os x/i.test(navigator.userAgent);
|
|||
const attrs = useAttrs();
|
||||
|
||||
const buttonText = computed(() => {
|
||||
return `${props.text} ${isMac ? "⌘" : "Ctrl"} + ↵`;
|
||||
return t("core.components.submit_button.computed_text", {
|
||||
text: props.text,
|
||||
shortcut: `${isMac ? "⌘" : "Ctrl"} + ↵`,
|
||||
});
|
||||
});
|
||||
|
||||
const { Command_Enter, Ctrl_Enter } = useMagicKeys();
|
||||
|
|
|
@ -79,7 +79,7 @@ const searchResults = computed(() => {
|
|||
<FormKit
|
||||
id="categoryDropdownSelectorInput"
|
||||
v-model="keyword"
|
||||
placeholder="输入关键词搜索"
|
||||
:placeholder="$t('core.common.placeholder.search')"
|
||||
type="text"
|
||||
></FormKit>
|
||||
</div>
|
||||
|
@ -107,7 +107,11 @@ const searchResults = computed(() => {
|
|||
</template>
|
||||
<template #end>
|
||||
<VEntityField
|
||||
:description="`${category.status?.postCount || 0} 篇文章`"
|
||||
:description="
|
||||
$t('core.common.fields.post_count', {
|
||||
count: category.status?.postCount || 0,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</VEntity>
|
||||
|
|
|
@ -77,7 +77,7 @@ const searchResults = computed(() => {
|
|||
<FormKit
|
||||
id="tagDropdownSelectorInput"
|
||||
v-model="keyword"
|
||||
placeholder="输入关键词搜索"
|
||||
:placeholder="$t('core.common.placeholder.search')"
|
||||
type="text"
|
||||
></FormKit>
|
||||
</div>
|
||||
|
@ -104,7 +104,11 @@ const searchResults = computed(() => {
|
|||
</template>
|
||||
<template #end>
|
||||
<VEntityField
|
||||
:description="`${tag.status?.postCount || 0} 篇文章`"
|
||||
:description="
|
||||
$t('core.common.fields.post_count', {
|
||||
count: tag.status?.postCount || 0,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</VEntity>
|
||||
|
|
|
@ -74,7 +74,7 @@ const searchResults = computed(() => {
|
|||
<FormKit
|
||||
id="userDropdownSelectorInput"
|
||||
v-model="keyword"
|
||||
placeholder="输入关键词搜索"
|
||||
:placeholder="$t('core.common.placeholder.search')"
|
||||
type="text"
|
||||
></FormKit>
|
||||
</div>
|
||||
|
|
|
@ -110,6 +110,9 @@ import * as fastq from "fastq";
|
|||
import type { queueAsPromised } from "fastq";
|
||||
import type { Attachment } from "@halo-dev/api-client";
|
||||
import { useFetchAttachmentPolicy } from "@/modules/contents/attachments/composables/use-attachment-policy";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -195,7 +198,9 @@ const editor = useEditor({
|
|||
ExtensionSubscript,
|
||||
ExtensionSuperscript,
|
||||
ExtensionPlaceholder.configure({
|
||||
placeholder: "输入 / 以选择输入类型",
|
||||
placeholder: t(
|
||||
"core.components.default_editor.extensions.placeholder.options.placeholder"
|
||||
),
|
||||
}),
|
||||
ExtensionHighlight,
|
||||
ExtensionCommands.configure({
|
||||
|
@ -356,7 +361,11 @@ const uploadQueue: queueAsPromised<Task> = fastq.promise(asyncWorker, 1);
|
|||
|
||||
async function asyncWorker(arg: Task): Promise<void> {
|
||||
if (!policies.value?.length) {
|
||||
Toast.warning("目前没有可用的存储策略");
|
||||
Toast.warning(
|
||||
t(
|
||||
"core.components.default_editor.upload_attachment.toast.no_available_policy"
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -377,7 +386,12 @@ const handleFetchPermalink = async (
|
|||
maxRetry: number
|
||||
): Promise<string | undefined> => {
|
||||
if (maxRetry === 0) {
|
||||
Toast.error(`获取附件永久链接失败:${attachment.spec.displayName}`);
|
||||
Toast.error(
|
||||
t(
|
||||
"core.components.default_editor.upload_attachment.toast.failed_fetch_permalink",
|
||||
{ display_name: attachment.spec.displayName }
|
||||
)
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
@ -433,7 +447,7 @@ const toolbarMenuItems = computed(() => {
|
|||
{
|
||||
type: "button",
|
||||
icon: markRaw(MdiFileImageBox),
|
||||
title: "插入附件",
|
||||
title: t("core.components.default_editor.toolbar.attachment"),
|
||||
action: () => (attachmentSelectorModal.value = true),
|
||||
isActive: () => false,
|
||||
},
|
||||
|
@ -536,7 +550,10 @@ watch(
|
|||
<template #extra>
|
||||
<div class="h-full w-72 overflow-y-auto border-l bg-white">
|
||||
<VTabs v-model:active-id="extraActiveId" type="outline">
|
||||
<VTabItem id="toc" label="大纲">
|
||||
<VTabItem
|
||||
id="toc"
|
||||
:label="$t('core.components.default_editor.tabs.toc.title')"
|
||||
>
|
||||
<div class="p-1 pt-0">
|
||||
<ul v-if="headingNodes?.length" class="space-y-1">
|
||||
<li
|
||||
|
@ -566,11 +583,16 @@ watch(
|
|||
</li>
|
||||
</ul>
|
||||
<div v-else class="flex flex-col items-center py-10">
|
||||
<span class="text-sm text-gray-600">暂无大纲</span>
|
||||
<span class="text-sm text-gray-600">
|
||||
{{ $t("core.components.default_editor.tabs.toc.empty") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</VTabItem>
|
||||
<VTabItem id="information" label="详情">
|
||||
<VTabItem
|
||||
id="information"
|
||||
:label="$t('core.components.default_editor.tabs.detail.title')"
|
||||
>
|
||||
<div class="flex flex-col gap-2 p-1 pt-0">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div
|
||||
|
@ -580,7 +602,11 @@ watch(
|
|||
<div
|
||||
class="text-sm text-gray-500 group-hover:text-gray-900"
|
||||
>
|
||||
字符数
|
||||
{{
|
||||
$t(
|
||||
"core.components.default_editor.tabs.detail.fields.character_count"
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<div class="rounded bg-gray-200 p-0.5">
|
||||
<IconCharacterRecognition
|
||||
|
@ -599,7 +625,11 @@ watch(
|
|||
<div
|
||||
class="text-sm text-gray-500 group-hover:text-gray-900"
|
||||
>
|
||||
词数
|
||||
{{
|
||||
$t(
|
||||
"core.components.default_editor.tabs.detail.fields.word_count"
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<div class="rounded bg-gray-200 p-0.5">
|
||||
<IconCharacterRecognition
|
||||
|
@ -621,7 +651,11 @@ watch(
|
|||
<div
|
||||
class="text-sm text-gray-500 group-hover:text-gray-900"
|
||||
>
|
||||
发布时间
|
||||
{{
|
||||
$t(
|
||||
"core.components.default_editor.tabs.detail.fields.publish_time"
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<div class="rounded bg-gray-200 p-0.5">
|
||||
<IconCalendar
|
||||
|
@ -630,7 +664,12 @@ watch(
|
|||
</div>
|
||||
</div>
|
||||
<div class="text-base font-medium text-gray-900">
|
||||
{{ formatDatetime(publishTime) || "未发布" }}
|
||||
{{
|
||||
formatDatetime(publishTime) ||
|
||||
$t(
|
||||
"core.components.default_editor.tabs.detail.fields.draft"
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -642,7 +681,11 @@ watch(
|
|||
<div
|
||||
class="text-sm text-gray-500 group-hover:text-gray-900"
|
||||
>
|
||||
创建者
|
||||
{{
|
||||
$t(
|
||||
"core.components.default_editor.tabs.detail.fields.owner"
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<div class="rounded bg-gray-200 p-0.5">
|
||||
<IconUserFollow
|
||||
|
@ -663,7 +706,11 @@ watch(
|
|||
<div
|
||||
class="text-sm text-gray-500 group-hover:text-gray-900"
|
||||
>
|
||||
访问链接
|
||||
{{
|
||||
$t(
|
||||
"core.components.default_editor.tabs.detail.fields.permalink"
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<div class="rounded bg-gray-200 p-0.5">
|
||||
<IconLink
|
||||
|
|
|
@ -213,7 +213,11 @@ defineExpose({
|
|||
@submit-invalid="onCustomFormSubmitCheck"
|
||||
@submit="customFormInvalid = false"
|
||||
>
|
||||
<FormKit v-model="customAnnotationsState" type="repeater" label="自定义">
|
||||
<FormKit
|
||||
v-model="customAnnotationsState"
|
||||
type="repeater"
|
||||
:label="$t('core.components.annotations_form.custom_fields.label')"
|
||||
>
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Key"
|
||||
|
@ -221,7 +225,9 @@ defineExpose({
|
|||
validation="required|keyValidationRule"
|
||||
:validation-rules="{ keyValidationRule }"
|
||||
:validation-messages="{
|
||||
keyValidationRule: '当前 Key 已被占用',
|
||||
keyValidationRule: $t(
|
||||
'core.components.annotations_form.custom_fields.validation'
|
||||
),
|
||||
}"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
|
|
|
@ -18,9 +18,11 @@ import { apiClient } from "@/utils/api-client";
|
|||
import { usePermission } from "@/utils/permission";
|
||||
import { useThemeStore } from "@/stores/theme";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
const { activatedTheme } = storeToRefs(useThemeStore());
|
||||
|
@ -78,7 +80,7 @@ const handleBuildSearchIndex = () => {
|
|||
icon: {
|
||||
component: markRaw(IconLink),
|
||||
},
|
||||
group: "后台页面",
|
||||
group: t("core.components.global_search.groups.console"),
|
||||
route,
|
||||
});
|
||||
});
|
||||
|
@ -91,7 +93,7 @@ const handleBuildSearchIndex = () => {
|
|||
icon: {
|
||||
component: markRaw(IconUserSettings),
|
||||
},
|
||||
group: "用户",
|
||||
group: t("core.components.global_search.groups.user"),
|
||||
route: {
|
||||
name: "UserDetail",
|
||||
params: {
|
||||
|
@ -113,7 +115,7 @@ const handleBuildSearchIndex = () => {
|
|||
icon: {
|
||||
src: plugin.status?.logo as string,
|
||||
},
|
||||
group: "插件",
|
||||
group: t("core.components.global_search.groups.plugin"),
|
||||
route: {
|
||||
name: "PluginDetail",
|
||||
params: {
|
||||
|
@ -135,7 +137,7 @@ const handleBuildSearchIndex = () => {
|
|||
icon: {
|
||||
component: markRaw(IconBookRead),
|
||||
},
|
||||
group: "文章",
|
||||
group: t("core.components.global_search.groups.post"),
|
||||
route: {
|
||||
name: "PostEditor",
|
||||
query: {
|
||||
|
@ -155,7 +157,7 @@ const handleBuildSearchIndex = () => {
|
|||
icon: {
|
||||
component: markRaw(IconBookRead),
|
||||
},
|
||||
group: "分类",
|
||||
group: t("core.components.global_search.groups.category"),
|
||||
route: {
|
||||
name: "Categories",
|
||||
query: {
|
||||
|
@ -173,7 +175,7 @@ const handleBuildSearchIndex = () => {
|
|||
icon: {
|
||||
component: markRaw(IconBookRead),
|
||||
},
|
||||
group: "标签",
|
||||
group: t("core.components.global_search.groups.tag"),
|
||||
route: {
|
||||
name: "Tags",
|
||||
query: {
|
||||
|
@ -195,7 +197,7 @@ const handleBuildSearchIndex = () => {
|
|||
icon: {
|
||||
component: markRaw(IconPages),
|
||||
},
|
||||
group: "自定义页面",
|
||||
group: t("core.components.global_search.groups.page"),
|
||||
route: {
|
||||
name: "SinglePageEditor",
|
||||
query: {
|
||||
|
@ -217,7 +219,7 @@ const handleBuildSearchIndex = () => {
|
|||
icon: {
|
||||
component: markRaw(IconFolder),
|
||||
},
|
||||
group: "附件",
|
||||
group: t("core.components.global_search.groups.attachment"),
|
||||
route: {
|
||||
name: "Attachments",
|
||||
query: {
|
||||
|
@ -242,7 +244,7 @@ const handleBuildSearchIndex = () => {
|
|||
icon: {
|
||||
component: markRaw(IconSettings),
|
||||
},
|
||||
group: "设置",
|
||||
group: t("core.components.global_search.groups.setting"),
|
||||
route: {
|
||||
name: "SystemSetting",
|
||||
params: {
|
||||
|
@ -266,7 +268,7 @@ const handleBuildSearchIndex = () => {
|
|||
icon: {
|
||||
component: markRaw(IconPalette),
|
||||
},
|
||||
group: "主题设置",
|
||||
group: t("core.components.global_search.groups.theme_setting"),
|
||||
route: {
|
||||
name: "ThemeSetting",
|
||||
params: {
|
||||
|
@ -379,7 +381,7 @@ const onVisibleChange = (visible: boolean) => {
|
|||
<input
|
||||
ref="globalSearchInput"
|
||||
v-model="keyword"
|
||||
placeholder="输入关键词以搜索"
|
||||
:placeholder="$t('core.components.global_search.placeholder')"
|
||||
class="w-full py-1 text-base outline-none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
|
@ -391,7 +393,7 @@ const onVisibleChange = (visible: boolean) => {
|
|||
v-if="!searchResults.length"
|
||||
class="flex items-center justify-center text-sm text-gray-500"
|
||||
>
|
||||
<span>没有搜索结果</span>
|
||||
<span>{{ $t("core.components.global_search.no_results") }}</span>
|
||||
</div>
|
||||
<ul
|
||||
v-if="searchResults.length > 0"
|
||||
|
@ -436,7 +438,9 @@ const onVisibleChange = (visible: boolean) => {
|
|||
</div>
|
||||
<div class="border-t border-gray-100 px-4 py-2.5">
|
||||
<div class="flex items-center justify-end">
|
||||
<span class="mr-1 text-xs text-gray-600">选择</span>
|
||||
<span class="mr-1 text-xs text-gray-600">
|
||||
{{ $t("core.components.global_search.buttons.select") }}
|
||||
</span>
|
||||
<kbd
|
||||
class="mr-1 w-5 rounded border p-0.5 text-center text-[10px] text-gray-600 shadow-sm"
|
||||
>
|
||||
|
@ -447,13 +451,17 @@ const onVisibleChange = (visible: boolean) => {
|
|||
>
|
||||
↓
|
||||
</kbd>
|
||||
<span class="mr-1 text-xs text-gray-600">确认</span>
|
||||
<span class="mr-1 text-xs text-gray-600">
|
||||
{{ $t("core.common.buttons.confirm") }}
|
||||
</span>
|
||||
<kbd
|
||||
class="mr-5 rounded border p-0.5 text-[10px] text-gray-600 shadow-sm"
|
||||
>
|
||||
Enter
|
||||
</kbd>
|
||||
<span class="mr-1 text-xs text-gray-600">关闭</span>
|
||||
<span class="mr-1 text-xs text-gray-600">
|
||||
{{ $t("core.common.buttons.close") }}
|
||||
</span>
|
||||
<kbd class="rounded border p-0.5 text-[10px] text-gray-600 shadow-sm">
|
||||
Esc
|
||||
</kbd>
|
||||
|
|
|
@ -10,12 +10,15 @@ import qs from "qs";
|
|||
import { submitForm } from "@formkit/core";
|
||||
import { JSEncrypt } from "jsencrypt";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import type {
|
||||
GlobalInfo,
|
||||
SocialAuthProvider,
|
||||
} from "@/modules/system/actuator/types";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "succeed"): void;
|
||||
}>();
|
||||
|
@ -75,19 +78,21 @@ const handleLogin = async () => {
|
|||
|
||||
if (e instanceof AxiosError) {
|
||||
if (/Network Error/.test(e.message)) {
|
||||
Toast.error("网络错误,请检查网络连接");
|
||||
Toast.error(t("core.common.toast.network_error"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.response?.status === 403) {
|
||||
Toast.warning("CSRF Token 失效,请重新尝试", { duration: 5000 });
|
||||
Toast.warning(t("core.login.operations.submit.toast_csrf"), {
|
||||
duration: 5000,
|
||||
});
|
||||
await handleGenerateToken();
|
||||
return;
|
||||
}
|
||||
|
||||
Toast.error("登录失败,用户名或密码错误");
|
||||
Toast.error(t("core.login.operations.submit.toast_failed"));
|
||||
} else {
|
||||
Toast.error("未知异常");
|
||||
Toast.error(t("core.common.toast.unknown_error"));
|
||||
}
|
||||
|
||||
loginForm.value.password = "";
|
||||
|
@ -132,10 +137,10 @@ const { data: socialAuthProviders } = useQuery<SocialAuthProvider[]>({
|
|||
>
|
||||
<FormKit
|
||||
:validation-messages="{
|
||||
required: '请输入用户名',
|
||||
required: $t('core.login.fields.username.validation'),
|
||||
}"
|
||||
name="username"
|
||||
placeholder="用户名"
|
||||
:placeholder="$t('core.login.fields.username.placeholder')"
|
||||
:autofocus="true"
|
||||
type="text"
|
||||
validation="required"
|
||||
|
@ -144,10 +149,10 @@ const { data: socialAuthProviders } = useQuery<SocialAuthProvider[]>({
|
|||
<FormKit
|
||||
id="passwordInput"
|
||||
:validation-messages="{
|
||||
required: '请输入密码',
|
||||
required: $t('core.login.fields.password.validation'),
|
||||
}"
|
||||
name="password"
|
||||
placeholder="密码"
|
||||
:placeholder="$t('core.login.fields.password.placeholder')"
|
||||
type="password"
|
||||
validation="required"
|
||||
>
|
||||
|
@ -160,11 +165,13 @@ const { data: socialAuthProviders } = useQuery<SocialAuthProvider[]>({
|
|||
type="secondary"
|
||||
@click="submitForm('login-form')"
|
||||
>
|
||||
登录
|
||||
{{ $t("core.login.button") }}
|
||||
</VButton>
|
||||
|
||||
<div v-if="socialAuthProviders?.length" class="mt-3 flex items-center">
|
||||
<span class="text-sm text-slate-600">其他登录:</span>
|
||||
<span class="text-sm text-slate-600">
|
||||
{{ $t("core.login.other_login") }}
|
||||
</span>
|
||||
<ul class="flex items-center">
|
||||
<li
|
||||
v-for="(socialAuthProvider, index) in socialAuthProviders"
|
||||
|
|
|
@ -2,8 +2,10 @@
|
|||
import { Toast, VModal } from "@halo-dev/components";
|
||||
import LoginForm from "@/components/login/LoginForm.vue";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const userStore = useUserStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const onVisibleChange = (visible: boolean) => {
|
||||
userStore.loginModalVisible = visible;
|
||||
|
@ -11,7 +13,7 @@ const onVisibleChange = (visible: boolean) => {
|
|||
|
||||
const onLoginSucceed = () => {
|
||||
onVisibleChange(false);
|
||||
Toast.success("登录成功");
|
||||
Toast.success(t("core.login.operations.submit.toast_success"));
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -21,7 +23,7 @@ const onLoginSucceed = () => {
|
|||
:mount-to-body="true"
|
||||
:width="400"
|
||||
:centered="true"
|
||||
title="重新登录"
|
||||
:title="$t('core.login.modal.title')"
|
||||
@update:visible="onVisibleChange"
|
||||
>
|
||||
<LoginForm v-if="userStore.loginModalVisible" @succeed="onLoginSucceed" />
|
||||
|
|
|
@ -4,6 +4,7 @@ import type { MenuGroupType, MenuItemType } from "@halo-dev/console-shared";
|
|||
import { VMenu, VMenuItem, VMenuLabel } from "@halo-dev/components";
|
||||
import type { RouteLocationMatched } from "vue-router";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const RoutesMenu = defineComponent({
|
||||
name: "RoutesMenu",
|
||||
|
@ -33,6 +34,8 @@ const RoutesMenu = defineComponent({
|
|||
return <icon height="20px" width="20px" />;
|
||||
}
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
function renderItems(items: MenuItemType[] | undefined) {
|
||||
return items?.map((item) => {
|
||||
return (
|
||||
|
@ -41,7 +44,7 @@ const RoutesMenu = defineComponent({
|
|||
<VMenuItem
|
||||
key={item.path}
|
||||
id={item.path}
|
||||
title={item.name}
|
||||
title={t(item.name, item.name)}
|
||||
v-slots={{
|
||||
icon: () => renderIcon(item.icon),
|
||||
}}
|
||||
|
@ -52,7 +55,7 @@ const RoutesMenu = defineComponent({
|
|||
<VMenuItem
|
||||
key={item.path}
|
||||
id={item.path}
|
||||
title={item.name}
|
||||
title={t(item.name, item.name)}
|
||||
v-slots={{
|
||||
icon: () => renderIcon(item.icon),
|
||||
}}
|
||||
|
@ -70,7 +73,7 @@ const RoutesMenu = defineComponent({
|
|||
{props.menus?.map((menu: MenuGroupType) => {
|
||||
return (
|
||||
<>
|
||||
{menu.name && <VMenuLabel>{menu.name}</VMenuLabel>}
|
||||
{menu.name && <VMenuLabel>{t(menu.name, menu.name)}</VMenuLabel>}
|
||||
{menu.items?.length && renderItems(menu.items)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -7,6 +7,7 @@ interface ContentCache {
|
|||
content?: string;
|
||||
}
|
||||
import debounce from "lodash.debounce";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
interface useContentCacheReturn {
|
||||
handleResetCache: () => void;
|
||||
|
@ -20,6 +21,7 @@ export function useContentCache(
|
|||
raw: Ref<string | undefined>
|
||||
): useContentCacheReturn {
|
||||
const content_caches = useLocalStorage<ContentCache[]>(key, []);
|
||||
const { t } = useI18n();
|
||||
|
||||
const handleResetCache = () => {
|
||||
if (name) {
|
||||
|
@ -27,7 +29,7 @@ export function useContentCache(
|
|||
(c: ContentCache) => c.name === name
|
||||
);
|
||||
if (cache) {
|
||||
Toast.info("已从缓存中恢复未保存的内容");
|
||||
Toast.info(t("core.composables.content_cache.toast_recovered"));
|
||||
raw.value = cache.content;
|
||||
}
|
||||
} else {
|
||||
|
@ -35,7 +37,7 @@ export function useContentCache(
|
|||
(c: ContentCache) => c.name === "" && c.content
|
||||
);
|
||||
if (cache) {
|
||||
Toast.info("已从缓存中恢复未保存的内容");
|
||||
Toast.info(t("core.composables.content_cache.toast_recovered"));
|
||||
raw.value = cache.content;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import type { PluginModule } from "@/stores/plugin";
|
|||
import { onMounted, ref, type Ref, defineAsyncComponent } from "vue";
|
||||
import { VLoading } from "@halo-dev/components";
|
||||
import Logo from "@/assets/logo.png";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
export interface EditorProvider extends EditorProviderRaw {
|
||||
logo?: string;
|
||||
|
@ -16,11 +17,12 @@ interface useEditorExtensionPointsReturn {
|
|||
export function useEditorExtensionPoints(): useEditorExtensionPointsReturn {
|
||||
// resolve plugin extension points
|
||||
const { pluginModules } = usePluginModuleStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const editorProviders = ref<EditorProvider[]>([
|
||||
{
|
||||
name: "default",
|
||||
displayName: "默认编辑器",
|
||||
displayName: t("core.plugin.extension_points.editor.providers.default"),
|
||||
component: defineAsyncComponent({
|
||||
loader: () => import("@/components/editor/DefaultEditor.vue"),
|
||||
loadingComponent: VLoading,
|
||||
|
|
|
@ -10,6 +10,7 @@ import merge from "lodash.merge";
|
|||
import type { ConfigMap, Setting, SettingForm } from "@halo-dev/api-client";
|
||||
import type { FormKitSchemaCondition, FormKitSchemaNode } from "@formkit/core";
|
||||
import { Toast } from "@halo-dev/components";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const initialConfigMap: ConfigMap = {
|
||||
apiVersion: "v1alpha1",
|
||||
|
@ -35,6 +36,8 @@ export function useSettingForm(
|
|||
settingName: Ref<string | undefined>,
|
||||
configMapName: Ref<string | undefined>
|
||||
): useSettingFormReturn {
|
||||
const { t } = useI18n();
|
||||
|
||||
const setting = ref<Setting>();
|
||||
const configMap = ref<ConfigMap>(cloneDeep(initialConfigMap));
|
||||
const configMapFormData = ref<
|
||||
|
@ -152,7 +155,7 @@ export function useSettingForm(
|
|||
configMapName.value = data.metadata.name;
|
||||
}
|
||||
|
||||
Toast.success("保存成功");
|
||||
Toast.success(t("core.common.toast.save_success"));
|
||||
} catch (e) {
|
||||
console.error("Failed to save configMap", e);
|
||||
} finally {
|
||||
|
|
|
@ -294,7 +294,9 @@ const handleDelete = () => {
|
|||
class="group flex cursor-pointer items-center justify-between rounded bg-gray-100 p-2"
|
||||
>
|
||||
<span class="text-xs text-gray-700 group-hover:text-gray-900">
|
||||
创建 {{ text }} 分类
|
||||
{{
|
||||
$t("core.formkit.category_select.creation_label", { text: text })
|
||||
}}
|
||||
</span>
|
||||
</li>
|
||||
<template v-if="text">
|
||||
|
|
|
@ -111,7 +111,7 @@ const handleMoveDown = (index: number) => {
|
|||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
添加
|
||||
{{ $t("core.common.buttons.add") }}
|
||||
</VButton>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -285,7 +285,7 @@ const handleDelete = () => {
|
|||
@click="handleCreateTag"
|
||||
>
|
||||
<span class="text-xs text-gray-700 group-hover:text-gray-900">
|
||||
创建 {{ text }} 标签
|
||||
{{ $t("core.formkit.tag_select.creation_label", { text: text }) }}
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
|
|
|
@ -30,9 +30,11 @@ import { useUserStore } from "@/stores/user";
|
|||
import { rbacAnnotations } from "@/constants/annotations";
|
||||
import { useScroll } from "@vueuse/core";
|
||||
import { defineStore, storeToRefs } from "pinia";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
||||
const moreMenuVisible = ref(false);
|
||||
const moreMenuRootVisible = ref(false);
|
||||
|
@ -43,7 +45,9 @@ const { currentRoles, currentUser } = storeToRefs(userStore);
|
|||
|
||||
const handleLogout = () => {
|
||||
Dialog.warning({
|
||||
title: "确定要退出登录吗?",
|
||||
title: t("core.sidebar.operations.logout.title"),
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await axios.post(`${import.meta.env.VITE_API_URL}/logout`, undefined, {
|
||||
|
@ -213,7 +217,11 @@ onMounted(() => {
|
|||
class="navbar fixed hidden h-full overflow-y-auto md:flex md:flex-col"
|
||||
>
|
||||
<div class="logo flex justify-center pt-5 pb-7">
|
||||
<a href="/" target="_blank" title="访问首页">
|
||||
<a
|
||||
href="/"
|
||||
target="_blank"
|
||||
:title="$t('core.sidebar.operations.visit_homepage.title')"
|
||||
>
|
||||
<IconLogo
|
||||
class="cursor-pointer select-none transition-all hover:brightness-125"
|
||||
/>
|
||||
|
@ -228,7 +236,9 @@ onMounted(() => {
|
|||
<span class="mr-3">
|
||||
<IconSearch />
|
||||
</span>
|
||||
<span class="flex-1 select-none text-base font-normal">搜索</span>
|
||||
<span class="flex-1 select-none text-base font-normal">
|
||||
{{ $t("core.sidebar.search.placeholder") }}
|
||||
</span>
|
||||
<div class="text-sm">
|
||||
{{ `${isMac ? "⌘" : "Ctrl"}+K` }}
|
||||
</div>
|
||||
|
@ -279,7 +289,7 @@ onMounted(() => {
|
|||
params: { name: '-' },
|
||||
}"
|
||||
>
|
||||
个人资料
|
||||
{{ $t("core.sidebar.operations.profile.button") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-close-popper
|
||||
|
@ -287,7 +297,7 @@ onMounted(() => {
|
|||
type="default"
|
||||
@click="handleLogout"
|
||||
>
|
||||
退出登录
|
||||
{{ $t("core.sidebar.operations.logout.button") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</div>
|
||||
|
@ -339,7 +349,9 @@ onMounted(() => {
|
|||
<div class="text-base">
|
||||
<IconMore />
|
||||
</div>
|
||||
<div class="mt-0.5 text-xs">更多</div>
|
||||
<div class="mt-0.5 text-xs">
|
||||
{{ $t("core.sidebar.operations.more.button") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,14 +1,34 @@
|
|||
import type { App } from "vue";
|
||||
import { createI18n } from "vue-i18n";
|
||||
import zh from "./lang/zh";
|
||||
// @ts-ignore
|
||||
import en from "./en.yaml";
|
||||
// @ts-ignore
|
||||
import zhCN from "./zh-CN.yaml";
|
||||
|
||||
const messages = {
|
||||
zh,
|
||||
en: en,
|
||||
zh: zhCN,
|
||||
"en-US": en,
|
||||
"zh-CN": zhCN,
|
||||
};
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: "zh",
|
||||
locale: "zh-CN",
|
||||
fallbackLocale: "zh-CN",
|
||||
messages,
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
export function getBrowserLanguage(): string {
|
||||
const browserLanguage = navigator.language;
|
||||
const language = messages[browserLanguage]
|
||||
? browserLanguage
|
||||
: browserLanguage.split("-")[0];
|
||||
return language in messages ? language : "zh-CN";
|
||||
}
|
||||
|
||||
export function setupI18n(app: App) {
|
||||
app.use(i18n);
|
||||
}
|
||||
|
||||
export { i18n };
|
||||
|
|
|
@ -1,73 +0,0 @@
|
|||
const zh = {
|
||||
rbac: {
|
||||
"Attachments Management": "附件",
|
||||
"Attachment Manage": "附件管理",
|
||||
"Attachment View": "附件查看",
|
||||
"role-template-view-attachments": "附件查看",
|
||||
|
||||
"Comments Management": "评论",
|
||||
"Comment Manage": "评论管理",
|
||||
"Comment View": "评论查看",
|
||||
"role-template-view-comments": "评论查看",
|
||||
|
||||
"ConfigMaps Management": "配置",
|
||||
"ConfigMap Manage": "配置管理",
|
||||
"ConfigMap View": "配置查看",
|
||||
"role-template-view-configmaps": "配置查看",
|
||||
|
||||
"Menus Management": "菜单",
|
||||
"Menu Manage": "菜单管理",
|
||||
"Menu View": "菜单查看",
|
||||
"role-template-view-menus": "菜单查看",
|
||||
|
||||
"Permissions Management": "权限",
|
||||
"Permissions Manage": "权限管理",
|
||||
"Permissions View": "权限查看",
|
||||
"role-template-view-permissions": "权限查看",
|
||||
"role-template-manage-permissions": "权限管理",
|
||||
|
||||
"Plugins Management": "插件",
|
||||
"Plugin Manage": "插件管理",
|
||||
"Plugin View": "插件查看",
|
||||
"role-template-view-plugins": "插件查看",
|
||||
|
||||
"Posts Management": "文章",
|
||||
"Post Manage": "文章管理",
|
||||
"Post View": "文章查看",
|
||||
"role-template-view-posts": "文章查看",
|
||||
"role-template-manage-snaphosts": "版本管理",
|
||||
"role-template-view-snaphosts": "版本查看",
|
||||
"role-template-manage-tags": "标签管理",
|
||||
"role-template-view-tags": "标签查看",
|
||||
"role-template-manage-categories": "分类管理",
|
||||
"role-template-view-categories": "分类查看",
|
||||
|
||||
"Roles Management": "角色",
|
||||
"Role Manage": "角色管理",
|
||||
"Role View": "角色查看",
|
||||
"role-template-view-roles": "角色查看",
|
||||
|
||||
"Settings Management": "设置表单",
|
||||
"Setting Manage": "设置表单管理",
|
||||
"Setting View": "设置表单查看",
|
||||
"role-template-view-settings": "设置表单查看",
|
||||
|
||||
"SinglePages Management": "页面",
|
||||
"SinglePage Manage": "页面管理",
|
||||
"SinglePage View": "页面查看",
|
||||
"role-template-view-singlepages": "页面查看",
|
||||
|
||||
"Themes Management": "主题",
|
||||
"Theme Manage": "主题管理",
|
||||
"Theme View": "主题查看",
|
||||
"role-template-view-themes": "主题查看",
|
||||
|
||||
"Users Management": "用户",
|
||||
"User manage": "用户管理",
|
||||
"User View": "用户查看",
|
||||
"role-template-view-users": "用户查看",
|
||||
"role-template-change-password": "修改密码",
|
||||
},
|
||||
};
|
||||
|
||||
export default zh;
|
File diff suppressed because it is too large
Load Diff
|
@ -9,6 +9,7 @@ import { apiClient } from "@/utils/api-client";
|
|||
// setup
|
||||
import "./setup/setupStyles";
|
||||
import { setupComponents } from "./setup/setupComponents";
|
||||
import { setupI18n, i18n, getBrowserLanguage } from "./locales";
|
||||
// core modules
|
||||
import { coreModules } from "./modules";
|
||||
import { useScriptTag } from "@vueuse/core";
|
||||
|
@ -20,15 +21,14 @@ import { useThemeStore } from "./stores/theme";
|
|||
import { useSystemStatesStore } from "./stores/system-states";
|
||||
import { useUserStore } from "./stores/user";
|
||||
import { useSystemConfigMapStore } from "./stores/system-configmap";
|
||||
import i18n from "./locales";
|
||||
import { VueQueryPlugin } from "@tanstack/vue-query";
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
setupComponents(app);
|
||||
setupI18n(app);
|
||||
|
||||
app.use(createPinia());
|
||||
app.use(i18n);
|
||||
app.use(VueQueryPlugin);
|
||||
|
||||
function registerModule(pluginModule: PluginModule, core: boolean) {
|
||||
|
@ -162,7 +162,10 @@ async function loadPluginModules() {
|
|||
});
|
||||
}
|
||||
} catch (e) {
|
||||
const message = `${plugin.metadata.name}: 加载插件入口文件失败`;
|
||||
const message = i18n.global.t(
|
||||
"core.plugin.loader.toast.entry_load_failed",
|
||||
{ name: plugin.spec.displayName }
|
||||
);
|
||||
console.error(message, e);
|
||||
pluginErrorMessages.push(message);
|
||||
}
|
||||
|
@ -172,7 +175,10 @@ async function loadPluginModules() {
|
|||
try {
|
||||
await loadStyle(`${import.meta.env.VITE_API_URL}${stylesheet}`);
|
||||
} catch (e) {
|
||||
const message = `${plugin.metadata.name}: 加载插件样式文件失败`;
|
||||
const message = i18n.global.t(
|
||||
"core.plugin.loader.toast.style_load_failed",
|
||||
{ name: plugin.spec.displayName }
|
||||
);
|
||||
console.error(message, e);
|
||||
pluginErrorMessages.push(message);
|
||||
}
|
||||
|
@ -238,6 +244,12 @@ async function initApp() {
|
|||
const userStore = useUserStore();
|
||||
await userStore.fetchCurrentUser();
|
||||
|
||||
// set locale
|
||||
// @ts-ignore
|
||||
i18n.global.locale.value =
|
||||
userStore.currentUser?.metadata.annotations?.["locale"] ||
|
||||
getBrowserLanguage();
|
||||
|
||||
if (userStore.isAnonymous) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -42,10 +42,12 @@ import { useRouteQuery } from "@vueuse/router";
|
|||
import { useFetchAttachmentGroup } from "./composables/use-attachment-group";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
import FilterTag from "@/components/filter/FilterTag.vue";
|
||||
import FilteCleanButton from "@/components/filter/FilterCleanButton.vue";
|
||||
import FilterCleanButton from "@/components/filter/FilterCleanButton.vue";
|
||||
import { getNode } from "@formkit/core";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
const { t } = useI18n();
|
||||
|
||||
const policyVisible = ref(false);
|
||||
const uploadVisible = ref(false);
|
||||
|
@ -64,19 +66,19 @@ interface SortItem {
|
|||
|
||||
const SortItems: SortItem[] = [
|
||||
{
|
||||
label: "较近上传",
|
||||
label: t("core.attachment.filters.sort.items.create_time_desc"),
|
||||
value: "creationTimestamp,desc",
|
||||
},
|
||||
{
|
||||
label: "较晚上传",
|
||||
label: t("core.attachment.filters.sort.items.create_time_asc"),
|
||||
value: "creationTimestamp,asc",
|
||||
},
|
||||
{
|
||||
label: "文件大小降序",
|
||||
label: t("core.attachment.filters.sort.items.size_desc"),
|
||||
value: "size,desc",
|
||||
},
|
||||
{
|
||||
label: "文件大小升序",
|
||||
label: t("core.attachment.filters.sort.items.size_asc"),
|
||||
value: "size,asc",
|
||||
},
|
||||
];
|
||||
|
@ -180,7 +182,7 @@ const handleMove = async (group: Group) => {
|
|||
await Promise.all(promises);
|
||||
selectedAttachments.value.clear();
|
||||
|
||||
Toast.success("移动成功");
|
||||
Toast.success(t("core.attachment.operations.move.toast_success"));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
|
@ -229,12 +231,12 @@ const getPolicyName = (name: string | undefined) => {
|
|||
const viewTypes = [
|
||||
{
|
||||
name: "list",
|
||||
tooltip: "列表模式",
|
||||
tooltip: t("core.attachment.filters.view_type.items.grid"),
|
||||
icon: IconList,
|
||||
},
|
||||
{
|
||||
name: "grid",
|
||||
tooltip: "网格模式",
|
||||
tooltip: t("core.attachment.filters.view_type.items.list"),
|
||||
icon: IconGrid,
|
||||
},
|
||||
];
|
||||
|
@ -299,7 +301,7 @@ onMounted(() => {
|
|||
@close="onUploadModalClose"
|
||||
/>
|
||||
<AttachmentPoliciesModal v-model:visible="policyVisible" />
|
||||
<VPageHeader title="附件库">
|
||||
<VPageHeader :title="$t('core.attachment.title')">
|
||||
<template #icon>
|
||||
<IconFolder class="mr-2 self-center" />
|
||||
</template>
|
||||
|
@ -313,7 +315,7 @@ onMounted(() => {
|
|||
<template #icon>
|
||||
<IconDatabase2Line class="h-full w-full" />
|
||||
</template>
|
||||
存储策略
|
||||
{{ $t("core.attachment.actions.storage_policies") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-permission="['system:attachments:manage']"
|
||||
|
@ -323,7 +325,7 @@ onMounted(() => {
|
|||
<template #icon>
|
||||
<IconUpload class="h-full w-full" />
|
||||
</template>
|
||||
上传
|
||||
{{ $t("core.common.buttons.upload") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
|
@ -357,7 +359,7 @@ onMounted(() => {
|
|||
<FormKit
|
||||
id="keywordInput"
|
||||
outer-class="!p-0"
|
||||
placeholder="输入关键词搜索"
|
||||
:placeholder="$t('core.common.placeholder.search')"
|
||||
type="text"
|
||||
name="keyword"
|
||||
:model-value="keyword"
|
||||
|
@ -365,44 +367,64 @@ onMounted(() => {
|
|||
></FormKit>
|
||||
|
||||
<FilterTag v-if="keyword" @close="handleClearKeyword()">
|
||||
关键词:{{ keyword }}
|
||||
{{
|
||||
$t("core.common.filters.results.keyword", {
|
||||
keyword: keyword,
|
||||
})
|
||||
}}
|
||||
</FilterTag>
|
||||
|
||||
<FilterTag
|
||||
v-if="selectedPolicy"
|
||||
@close="handleSelectPolicy(undefined)"
|
||||
>
|
||||
存储策略:{{ selectedPolicy?.spec.displayName }}
|
||||
{{
|
||||
$t("core.attachment.filters.storage_policy.result", {
|
||||
storage_policy: selectedPolicy.spec.displayName,
|
||||
})
|
||||
}}
|
||||
</FilterTag>
|
||||
|
||||
<FilterTag
|
||||
v-if="selectedUser"
|
||||
@close="handleSelectUser(undefined)"
|
||||
>
|
||||
上传者:{{ selectedUser?.spec.displayName }}
|
||||
{{
|
||||
$t("core.attachment.filters.owner.result", {
|
||||
owner: selectedUser.spec.displayName,
|
||||
})
|
||||
}}
|
||||
</FilterTag>
|
||||
|
||||
<FilterTag
|
||||
v-if="selectedSortItem"
|
||||
@click="handleSortItemChange()"
|
||||
>
|
||||
排序:{{ selectedSortItem.label }}
|
||||
{{
|
||||
$t("core.common.filters.results.sort", {
|
||||
sort: selectedSortItem.label,
|
||||
})
|
||||
}}
|
||||
</FilterTag>
|
||||
|
||||
<FilteCleanButton
|
||||
<FilterCleanButton
|
||||
v-if="hasFilters"
|
||||
@click="handleClearFilters"
|
||||
/>
|
||||
</div>
|
||||
<VSpace v-else>
|
||||
<VButton type="danger" @click="handleDeleteInBatch">
|
||||
删除
|
||||
{{ $t("core.common.buttons.delete") }}
|
||||
</VButton>
|
||||
<VButton @click="selectedAttachments.clear()">
|
||||
取消选择
|
||||
{{
|
||||
$t("core.attachment.operations.deselect_items.button")
|
||||
}}
|
||||
</VButton>
|
||||
<FloatingDropdown>
|
||||
<VButton>移动</VButton>
|
||||
<VButton>
|
||||
{{ $t("core.attachment.operations.move.button") }}
|
||||
</VButton>
|
||||
<template #popper>
|
||||
<div class="w-72 p-4">
|
||||
<ul class="space-y-1">
|
||||
|
@ -429,7 +451,11 @@ onMounted(() => {
|
|||
<div
|
||||
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
||||
>
|
||||
<span class="mr-0.5">存储策略</span>
|
||||
<span class="mr-0.5">
|
||||
{{
|
||||
$t("core.attachment.filters.storage_policy.label")
|
||||
}}
|
||||
</span>
|
||||
<span>
|
||||
<IconArrowDown />
|
||||
</span>
|
||||
|
@ -464,7 +490,9 @@ onMounted(() => {
|
|||
<div
|
||||
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
||||
>
|
||||
<span class="mr-0.5">上传者</span>
|
||||
<span class="mr-0.5">
|
||||
{{ $t("core.attachment.filters.owner.label") }}
|
||||
</span>
|
||||
<span>
|
||||
<IconArrowDown />
|
||||
</span>
|
||||
|
@ -503,7 +531,9 @@ onMounted(() => {
|
|||
<div
|
||||
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
||||
>
|
||||
<span class="mr-0.5">排序</span>
|
||||
<span class="mr-0.5">
|
||||
{{ $t("core.common.filters.labels.sort") }}
|
||||
</span>
|
||||
<span>
|
||||
<IconArrowDown />
|
||||
</span>
|
||||
|
@ -545,7 +575,7 @@ onMounted(() => {
|
|||
@click="handleFetchAttachments()"
|
||||
>
|
||||
<IconRefreshLine
|
||||
v-tooltip="`刷新`"
|
||||
v-tooltip="$t('core.common.buttons.refresh')"
|
||||
:class="{ 'animate-spin text-gray-900': isFetching }"
|
||||
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
|
||||
/>
|
||||
|
@ -570,12 +600,14 @@ onMounted(() => {
|
|||
|
||||
<Transition v-else-if="!attachments?.length" appear name="fade">
|
||||
<VEmpty
|
||||
message="当前分组没有附件,你可以尝试刷新或者上传附件"
|
||||
title="当前分组没有附件"
|
||||
:message="$t('core.attachment.empty.message')"
|
||||
:title="$t('core.attachment.empty.title')"
|
||||
>
|
||||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton @click="handleFetchAttachments">刷新</VButton>
|
||||
<VButton @click="handleFetchAttachments">
|
||||
{{ $t("core.common.buttons.refresh") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-permission="['system:attachments:manage']"
|
||||
type="secondary"
|
||||
|
@ -584,7 +616,7 @@ onMounted(() => {
|
|||
<template #icon>
|
||||
<IconUpload class="h-full w-full" />
|
||||
</template>
|
||||
上传附件
|
||||
{{ $t("core.attachment.empty.actions.upload") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
|
@ -624,14 +656,18 @@ onMounted(() => {
|
|||
<div
|
||||
class="flex h-full items-center justify-center object-cover"
|
||||
>
|
||||
<span class="text-xs text-gray-400">加载中...</span>
|
||||
<span class="text-xs text-gray-400">
|
||||
{{ $t("core.common.status.loading") }}...
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #error>
|
||||
<div
|
||||
class="flex h-full items-center justify-center object-cover"
|
||||
>
|
||||
<span class="text-xs text-red-400">加载异常</span>
|
||||
<span class="text-xs text-red-400">
|
||||
{{ $t("core.common.status.loading_error") }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</LazyImage>
|
||||
|
@ -652,7 +688,7 @@ onMounted(() => {
|
|||
v-if="attachment.metadata.deletionTimestamp"
|
||||
class="absolute top-1 right-1 text-xs text-red-300"
|
||||
>
|
||||
删除中...
|
||||
{{ $t("core.common.status.deleting") }}...
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
@ -748,7 +784,7 @@ onMounted(() => {
|
|||
>
|
||||
<template #description>
|
||||
<VStatusDot
|
||||
v-tooltip="`删除中`"
|
||||
v-tooltip="$t('core.common.status.deleting')"
|
||||
state="warning"
|
||||
animate
|
||||
/>
|
||||
|
@ -780,7 +816,7 @@ onMounted(() => {
|
|||
type="danger"
|
||||
@click="handleDelete(attachment)"
|
||||
>
|
||||
删除
|
||||
{{ $t("core.common.buttons.delete") }}
|
||||
</VButton>
|
||||
</template>
|
||||
</VEntity>
|
||||
|
@ -794,6 +830,8 @@ onMounted(() => {
|
|||
<VPagination
|
||||
v-model:page="page"
|
||||
v-model:size="size"
|
||||
:page-label="$t('core.components.pagination.page_label')"
|
||||
:size-label="$t('core.components.pagination.size_label')"
|
||||
:total="total"
|
||||
:size-options="[60, 120, 200]"
|
||||
/>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts" setup>
|
||||
import { VButton, VModal, VSpace, VTag } from "@halo-dev/components";
|
||||
import { VButton, VModal, VSpace } from "@halo-dev/components";
|
||||
import LazyImage from "@/components/image/LazyImage.vue";
|
||||
import type { Attachment } from "@halo-dev/api-client";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
|
@ -69,7 +69,11 @@ const onVisibleChange = (visible: boolean) => {
|
|||
</script>
|
||||
<template>
|
||||
<VModal
|
||||
:title="`附件:${attachment?.spec.displayName || ''}`"
|
||||
:title="
|
||||
$t('core.attachment.detail_modal.title', {
|
||||
display_name: attachment?.spec.displayName || '',
|
||||
})
|
||||
"
|
||||
:visible="visible"
|
||||
:width="1000"
|
||||
:mount-to-body="mountToBody"
|
||||
|
@ -86,7 +90,9 @@ const onVisibleChange = (visible: boolean) => {
|
|||
class="flex justify-center"
|
||||
>
|
||||
<img
|
||||
v-tooltip.bottom="`点击退出预览`"
|
||||
v-tooltip.bottom="
|
||||
$t('core.attachment.detail_modal.preview.click_to_exit')
|
||||
"
|
||||
:alt="attachment?.spec.displayName"
|
||||
:src="attachment?.status?.permalink"
|
||||
class="w-auto cursor-pointer rounded"
|
||||
|
@ -97,7 +103,9 @@ const onVisibleChange = (visible: boolean) => {
|
|||
<div
|
||||
class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"
|
||||
>
|
||||
<dt class="text-sm font-medium text-gray-900">预览</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.attachment.detail_modal.fields.preview") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||
<div
|
||||
v-if="isImage(attachment?.spec.mediaType)"
|
||||
|
@ -109,10 +117,14 @@ const onVisibleChange = (visible: boolean) => {
|
|||
classes="max-w-full cursor-pointer rounded sm:max-w-[50%]"
|
||||
>
|
||||
<template #loading>
|
||||
<span class="text-gray-400">加载中...</span>
|
||||
<span class="text-gray-400">
|
||||
{{ $t("core.common.status.loading") }}...
|
||||
</span>
|
||||
</template>
|
||||
<template #error>
|
||||
<span class="text-red-400">加载异常</span>
|
||||
<span class="text-red-400">
|
||||
{{ $t("core.common.status.loading_error") }}
|
||||
</span>
|
||||
</template>
|
||||
</LazyImage>
|
||||
</div>
|
||||
|
@ -122,61 +134,86 @@ const onVisibleChange = (visible: boolean) => {
|
|||
controls
|
||||
class="max-w-full rounded sm:max-w-[50%]"
|
||||
>
|
||||
当前浏览器不支持该视频播放
|
||||
{{
|
||||
$t("core.attachment.detail_modal.preview.video_not_support")
|
||||
}}
|
||||
</video>
|
||||
</div>
|
||||
<div v-else-if="attachment?.spec.mediaType?.startsWith('audio/')">
|
||||
<audio :src="attachment.status?.permalink" controls>
|
||||
当前浏览器不支持该音频播放
|
||||
{{
|
||||
$t("core.attachment.detail_modal.preview.audio_not_support")
|
||||
}}
|
||||
</audio>
|
||||
</div>
|
||||
<span v-else> 此文件不支持预览 </span>
|
||||
<span v-else>
|
||||
{{ $t("core.attachment.detail_modal.preview.not_support") }}
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium text-gray-900">存储策略</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.attachment.detail_modal.fields.storage_policy") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||
{{ policy?.spec.displayName }}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium text-gray-900">所在分组</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.attachment.detail_modal.fields.group") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||
{{ getGroupName(attachment?.spec.groupName) || "未分组" }}
|
||||
{{
|
||||
getGroupName(attachment?.spec.groupName) ||
|
||||
$t("core.attachment.common.text.ungrouped")
|
||||
}}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium text-gray-900">文件名称</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.attachment.detail_modal.fields.display_name") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||
{{ attachment?.spec.displayName }}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium text-gray-900">文件类型</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.attachment.detail_modal.fields.media_type") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||
{{ attachment?.spec.mediaType }}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium text-gray-900">文件大小</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.attachment.detail_modal.fields.size") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||
{{ prettyBytes(attachment?.spec.size || 0) }}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium text-gray-900">上传者</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.attachment.detail_modal.fields.owner") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||
{{ attachment?.spec.ownerName }}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium text-gray-900">上传时间</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.attachment.detail_modal.fields.creation_time") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||
{{ formatDatetime(attachment?.metadata.creationTimestamp) }}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium text-gray-900">原始链接</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.attachment.detail_modal.fields.permalink") }}
|
||||
</dt>
|
||||
<dd
|
||||
class="mt-1 text-sm text-gray-900 hover:text-blue-600 sm:col-span-2 sm:mt-0"
|
||||
>
|
||||
|
@ -185,63 +222,13 @@ const onVisibleChange = (visible: boolean) => {
|
|||
</a>
|
||||
</dd>
|
||||
</div>
|
||||
<!-- TODO: add attachment ref support -->
|
||||
<div
|
||||
v-if="false"
|
||||
class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"
|
||||
>
|
||||
<dt class="text-sm font-medium text-gray-900">引用位置</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||
// TODO
|
||||
<ul class="mt-2 space-y-2">
|
||||
<li>
|
||||
<div
|
||||
class="inline-flex w-96 cursor-pointer flex-row gap-x-3 rounded border p-3 hover:border-primary"
|
||||
>
|
||||
<RouterLink
|
||||
:to="{
|
||||
name: 'Posts',
|
||||
}"
|
||||
class="font-medium text-gray-900 hover:text-blue-400"
|
||||
>
|
||||
Halo 1.5.3 发布了
|
||||
</RouterLink>
|
||||
<div class="text-xs">
|
||||
<VSpace>
|
||||
<VTag>文章</VTag>
|
||||
</VSpace>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div
|
||||
class="inline-flex w-96 cursor-pointer flex-row gap-x-3 rounded border p-3 hover:border-primary"
|
||||
>
|
||||
<RouterLink
|
||||
:to="{
|
||||
name: 'Posts',
|
||||
}"
|
||||
class="font-medium text-gray-900 hover:text-blue-400"
|
||||
>
|
||||
Halo 1.5.2 发布
|
||||
</RouterLink>
|
||||
<div class="text-xs">
|
||||
<VSpace>
|
||||
<VTag>文章</VTag>
|
||||
</VSpace>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<template #footer>
|
||||
<VSpace>
|
||||
<VButton type="default" @click="onVisibleChange(false)"
|
||||
>关闭 Esc</VButton
|
||||
>
|
||||
<VButton type="default" @click="onVisibleChange(false)">
|
||||
{{ $t("core.common.buttons.close_and_shortcut") }}
|
||||
</VButton>
|
||||
<slot name="footer" />
|
||||
</VSpace>
|
||||
</template>
|
||||
|
|
|
@ -7,6 +7,7 @@ import cloneDeep from "lodash.clonedeep";
|
|||
import { apiClient } from "@/utils/api-client";
|
||||
import { reset } from "@formkit/core";
|
||||
import { setFocus } from "@/formkit/utils/focus";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -24,6 +25,8 @@ const emit = defineEmits<{
|
|||
(event: "close"): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const initialFormState: Group = {
|
||||
spec: {
|
||||
displayName: "",
|
||||
|
@ -44,7 +47,9 @@ const isUpdateMode = computed(() => {
|
|||
});
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
return isUpdateMode.value ? "编辑附件分组" : "新增附件分组";
|
||||
return isUpdateMode.value
|
||||
? t("core.attachment.group_editing_modal.titles.update")
|
||||
: t("core.attachment.group_editing_modal.titles.create");
|
||||
});
|
||||
|
||||
const handleSave = async () => {
|
||||
|
@ -65,7 +70,7 @@ const handleSave = async () => {
|
|||
);
|
||||
}
|
||||
|
||||
Toast.success("保存成功");
|
||||
Toast.success(t("core.common.toast.save_success"));
|
||||
onVisibleChange(false);
|
||||
} catch (e) {
|
||||
console.error("Failed to save attachment group", e);
|
||||
|
@ -126,7 +131,9 @@ watch(
|
|||
<FormKit
|
||||
id="displayNameInput"
|
||||
v-model="formState.spec.displayName"
|
||||
label="名称"
|
||||
:label="
|
||||
$t('core.attachment.group_editing_modal.fields.display_name.label')
|
||||
"
|
||||
type="text"
|
||||
name="displayName"
|
||||
validation="required|length:0,50"
|
||||
|
@ -138,10 +145,13 @@ watch(
|
|||
v-if="visible"
|
||||
:loading="saving"
|
||||
type="secondary"
|
||||
:text="$t('core.common.buttons.submit')"
|
||||
@submit="$formkit.submit('attachment-group-form')"
|
||||
>
|
||||
</SubmitButton>
|
||||
<VButton @click="onVisibleChange(false)">取消 Esc</VButton>
|
||||
<VButton @click="onVisibleChange(false)">
|
||||
{{ $t("core.common.buttons.cancel_and_shortcut") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
</VModal>
|
||||
|
|
|
@ -20,6 +20,9 @@ import type { Group } from "@halo-dev/api-client";
|
|||
import { useRouteQuery } from "@vueuse/router";
|
||||
import { useFetchAttachmentGroup } from "../composables/use-attachment-group";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -42,7 +45,7 @@ const emit = defineEmits<{
|
|||
const defaultGroups: Group[] = [
|
||||
{
|
||||
spec: {
|
||||
displayName: "全部",
|
||||
displayName: t("core.attachment.group_list.internal_groups.all"),
|
||||
},
|
||||
apiVersion: "",
|
||||
kind: "",
|
||||
|
@ -52,7 +55,7 @@ const defaultGroups: Group[] = [
|
|||
},
|
||||
{
|
||||
spec: {
|
||||
displayName: "未分组",
|
||||
displayName: t("core.attachment.common.text.ungrouped"),
|
||||
},
|
||||
apiVersion: "",
|
||||
kind: "",
|
||||
|
@ -91,9 +94,11 @@ const onEditingModalClose = () => {
|
|||
|
||||
const handleDelete = (group: Group) => {
|
||||
Dialog.warning({
|
||||
title: "确定要删除该分组吗?",
|
||||
description: "将删除分组,并将分组下的附件移动至未分组,该操作不可恢复。",
|
||||
title: t("core.attachment.group_list.operations.delete.title"),
|
||||
description: t("core.attachment.group_list.operations.delete.title"),
|
||||
confirmType: "danger",
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
// TODO: 后续将修改为在后端进行批量操作处理
|
||||
const { data } = await apiClient.attachment.searchAttachments({
|
||||
|
@ -123,16 +128,26 @@ const handleDelete = (group: Group) => {
|
|||
emit("reload-attachments");
|
||||
emit("update");
|
||||
|
||||
Toast.success(`删除成功,${data.total} 个附件已移动至未分组`);
|
||||
Toast.success(
|
||||
t("core.attachment.group_list.operations.delete.toast_success", {
|
||||
total: data.total,
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteWithAttachments = (group: Group) => {
|
||||
Dialog.warning({
|
||||
title: "确定要删除该分组吗?",
|
||||
description: "将删除分组以及分组下的所有附件,该操作不可恢复。",
|
||||
title: t(
|
||||
"core.attachment.group_list.operations.delete_with_attachments.title"
|
||||
),
|
||||
description: t(
|
||||
"core.attachment.group_list.operations.delete_with_attachments.description"
|
||||
),
|
||||
confirmType: "danger",
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
// TODO: 后续将修改为在后端进行批量操作处理
|
||||
const { data } = await apiClient.attachment.searchAttachments({
|
||||
|
@ -157,7 +172,12 @@ const handleDeleteWithAttachments = (group: Group) => {
|
|||
emit("reload-attachments");
|
||||
emit("update");
|
||||
|
||||
Toast.success(`删除成功,${data.total} 个附件已被同时删除`);
|
||||
Toast.success(
|
||||
t(
|
||||
"core.attachment.group_list.operations.delete_with_attachments.toast_success",
|
||||
{ total: data.total }
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -230,7 +250,7 @@ onMounted(async () => {
|
|||
</span>
|
||||
<VStatusDot
|
||||
v-if="group.metadata.deletionTimestamp"
|
||||
v-tooltip="`删除中`"
|
||||
v-tooltip="$t('core.common.status.deleting')"
|
||||
state="warning"
|
||||
animate
|
||||
/>
|
||||
|
@ -249,14 +269,16 @@ onMounted(async () => {
|
|||
type="secondary"
|
||||
@click="handleOpenEditingModal(group)"
|
||||
>
|
||||
重命名
|
||||
{{ $t("core.attachment.group_list.operations.rename.button") }}
|
||||
</VButton>
|
||||
<FloatingDropdown
|
||||
class="w-full"
|
||||
placement="right"
|
||||
:triggers="['click']"
|
||||
>
|
||||
<VButton block type="danger">删除</VButton>
|
||||
<VButton block type="danger">
|
||||
{{ $t("core.common.buttons.delete") }}
|
||||
</VButton>
|
||||
<template #popper>
|
||||
<div class="w-52 p-2">
|
||||
<VSpace class="w-full" direction="column">
|
||||
|
@ -267,7 +289,11 @@ onMounted(async () => {
|
|||
size="sm"
|
||||
@click="handleDelete(group)"
|
||||
>
|
||||
删除并将附件移动至未分组
|
||||
{{
|
||||
$t(
|
||||
"core.attachment.group_list.operations.delete.button"
|
||||
)
|
||||
}}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-close-popper.all
|
||||
|
@ -276,7 +302,11 @@ onMounted(async () => {
|
|||
size="sm"
|
||||
@click="handleDeleteWithAttachments(group)"
|
||||
>
|
||||
删除并同时删除附件
|
||||
{{
|
||||
$t(
|
||||
"core.attachment.group_list.operations.delete_with_attachments.button"
|
||||
)
|
||||
}}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</div>
|
||||
|
@ -294,7 +324,9 @@ onMounted(async () => {
|
|||
@click="editingModal = true"
|
||||
>
|
||||
<div class="flex flex-1 items-center truncate">
|
||||
<span class="truncate text-sm">添加分组</span>
|
||||
<span class="truncate text-sm">
|
||||
{{ $t("core.common.buttons.new") }}
|
||||
</span>
|
||||
</div>
|
||||
<IconAddCircle />
|
||||
</div>
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
useFetchAttachmentPolicyTemplate,
|
||||
} from "../composables/use-attachment-policy";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
|
@ -35,6 +36,8 @@ const emit = defineEmits<{
|
|||
(event: "close"): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { policies, isLoading, handleFetchPolicies } = useFetchAttachmentPolicy();
|
||||
const { policyTemplates } = useFetchAttachmentPolicyTemplate();
|
||||
|
||||
|
@ -78,21 +81,31 @@ const handleDelete = async (policy: Policy) => {
|
|||
|
||||
if (data.total > 0) {
|
||||
Dialog.warning({
|
||||
title: "删除失败",
|
||||
description: "该策略下存在附件,无法删除。",
|
||||
title: t(
|
||||
"core.attachment.policies_modal.operations.can_not_delete.title"
|
||||
),
|
||||
description: t(
|
||||
"core.attachment.policies_modal.operations.can_not_delete.description"
|
||||
),
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Dialog.warning({
|
||||
title: "确定要删除该策略吗?",
|
||||
description: "当前策略下没有已上传的附件。",
|
||||
title: t("core.attachment.policies_modal.operations.delete.title"),
|
||||
description: t(
|
||||
"core.attachment.policies_modal.operations.delete.description"
|
||||
),
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
await apiClient.extension.storage.policy.deletestorageHaloRunV1alpha1Policy(
|
||||
{ name: policy.metadata.name }
|
||||
);
|
||||
|
||||
Toast.success("删除成功");
|
||||
Toast.success(t("core.common.toast.delete_success"));
|
||||
handleFetchPolicies();
|
||||
},
|
||||
});
|
||||
|
@ -107,14 +120,14 @@ const onEditingModalClose = () => {
|
|||
<VModal
|
||||
:visible="visible"
|
||||
:width="750"
|
||||
title="存储策略"
|
||||
:title="$t('core.attachment.policies_modal.title')"
|
||||
:body-class="['!p-0']"
|
||||
:layer-closable="true"
|
||||
@update:visible="onVisibleChange"
|
||||
>
|
||||
<template #actions>
|
||||
<FloatingDropdown>
|
||||
<span v-tooltip="`添加存储策略`">
|
||||
<span v-tooltip="$t('core.common.buttons.new')">
|
||||
<IconAddCircle />
|
||||
</span>
|
||||
<template #popper>
|
||||
|
@ -138,18 +151,20 @@ const onEditingModalClose = () => {
|
|||
</template>
|
||||
<VEmpty
|
||||
v-if="!policies?.length && !isLoading"
|
||||
message="当前没有可用的存储策略,你可以尝试刷新或者新建策略"
|
||||
title="当前没有可用的存储策略"
|
||||
:message="$t('core.attachment.policies_modal.empty.message')"
|
||||
:title="$t('core.attachment.policies_modal.empty.title')"
|
||||
>
|
||||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton @click="handleFetchPolicies">刷新</VButton>
|
||||
<VButton @click="handleFetchPolicies">
|
||||
{{ $t("core.common.buttons.refresh") }}
|
||||
</VButton>
|
||||
<FloatingDropdown>
|
||||
<VButton type="secondary">
|
||||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
新建策略
|
||||
{{ $t("core.common.buttons.new") }}
|
||||
</VButton>
|
||||
<template #popper>
|
||||
<div class="w-72 p-4">
|
||||
|
@ -188,7 +203,11 @@ const onEditingModalClose = () => {
|
|||
<template #end>
|
||||
<VEntityField v-if="policy.metadata.deletionTimestamp">
|
||||
<template #description>
|
||||
<VStatusDot v-tooltip="`删除中`" state="warning" animate />
|
||||
<VStatusDot
|
||||
v-tooltip="$t('core.common.status.deleting')"
|
||||
state="warning"
|
||||
animate
|
||||
/>
|
||||
</template>
|
||||
</VEntityField>
|
||||
<VEntityField>
|
||||
|
@ -206,7 +225,7 @@ const onEditingModalClose = () => {
|
|||
type="secondary"
|
||||
@click="handleOpenEditingModal(policy)"
|
||||
>
|
||||
编辑
|
||||
{{ $t("core.common.buttons.edit") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-close-popper
|
||||
|
@ -214,14 +233,16 @@ const onEditingModalClose = () => {
|
|||
type="danger"
|
||||
@click="handleDelete(policy)"
|
||||
>
|
||||
删除
|
||||
{{ $t("core.common.buttons.delete") }}
|
||||
</VButton>
|
||||
</template>
|
||||
</VEntity>
|
||||
</li>
|
||||
</ul>
|
||||
<template #footer>
|
||||
<VButton @click="onVisibleChange(false)">关闭 Esc</VButton>
|
||||
<VButton @click="onVisibleChange(false)">
|
||||
{{ $t("core.common.buttons.close_and_shortcut") }}
|
||||
</VButton>
|
||||
</template>
|
||||
</VModal>
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
type FormKitSchemaNode,
|
||||
} from "@formkit/core";
|
||||
import { setFocus } from "@/formkit/utils/focus";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -29,6 +30,8 @@ const emit = defineEmits<{
|
|||
(event: "close"): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const initialFormState: Policy = {
|
||||
spec: {
|
||||
displayName: "",
|
||||
|
@ -97,8 +100,12 @@ const isUpdateMode = computed(() => {
|
|||
|
||||
const modalTitle = computed(() => {
|
||||
return isUpdateMode.value
|
||||
? `编辑策略:${props.policy?.spec.displayName}`
|
||||
: `新增策略:${policyTemplate.value?.spec?.displayName}`;
|
||||
? t("core.attachment.policy_editing_modal.titles.update", {
|
||||
policy: props.policy?.spec.displayName,
|
||||
})
|
||||
: t("core.attachment.policy_editing_modal.titles.create", {
|
||||
policy_template: policyTemplate.value?.spec?.displayName,
|
||||
});
|
||||
});
|
||||
|
||||
const handleSave = async () => {
|
||||
|
@ -128,7 +135,7 @@ const handleSave = async () => {
|
|||
);
|
||||
}
|
||||
|
||||
Toast.success("保存成功");
|
||||
Toast.success(t("core.common.toast.save_success"));
|
||||
onVisibleChange(false);
|
||||
} catch (e) {
|
||||
console.error("Failed to save attachment policy", e);
|
||||
|
@ -214,7 +221,9 @@ const onVisibleChange = (visible: boolean) => {
|
|||
<FormKit
|
||||
id="displayNameInput"
|
||||
v-model="formState.spec.displayName"
|
||||
label="名称"
|
||||
:label="
|
||||
$t('core.attachment.policy_editing_modal.fields.display_name.label')
|
||||
"
|
||||
type="text"
|
||||
name="displayName"
|
||||
validation="required|length:0,50"
|
||||
|
@ -231,10 +240,13 @@ const onVisibleChange = (visible: boolean) => {
|
|||
v-if="visible"
|
||||
:loading="saving"
|
||||
type="secondary"
|
||||
:text="$t('core.common.buttons.submit')"
|
||||
@submit="$formkit.submit('attachment-policy-form')"
|
||||
>
|
||||
</SubmitButton>
|
||||
<VButton @click="onVisibleChange(false)">取消 Esc</VButton>
|
||||
<VButton @click="onVisibleChange(false)">
|
||||
{{ $t("core.common.buttons.cancel_and_shortcut") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
</VModal>
|
||||
|
|
|
@ -8,6 +8,9 @@ import type {
|
|||
PluginModule,
|
||||
} from "@halo-dev/console-shared";
|
||||
import { usePluginModuleStore } from "@/stores/plugin";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
|
@ -29,7 +32,7 @@ const selected = ref<AttachmentLike[]>([] as AttachmentLike[]);
|
|||
const attachmentSelectProviders = ref<AttachmentSelectProvider[]>([
|
||||
{
|
||||
id: "core",
|
||||
label: "附件库",
|
||||
label: t("core.attachment.select_modal.providers.default.label"),
|
||||
component: markRaw(CoreSelectorProvider),
|
||||
},
|
||||
]);
|
||||
|
@ -88,7 +91,7 @@ const handleConfirm = () => {
|
|||
:width="1240"
|
||||
:mount-to-body="true"
|
||||
:layer-closable="true"
|
||||
title="选择附件"
|
||||
:title="$t('core.attachment.select_modal.title')"
|
||||
height="calc(100vh - 20px)"
|
||||
@update:visible="onVisibleChange"
|
||||
>
|
||||
|
@ -111,15 +114,21 @@ const handleConfirm = () => {
|
|||
v-model:selected="selected"
|
||||
@change-provider="onChangeProvider"
|
||||
></component>
|
||||
<template #fallback> 加载中 </template>
|
||||
<template #fallback>
|
||||
{{ $t("core.common.status.loading") }}
|
||||
</template>
|
||||
</Suspense>
|
||||
</template>
|
||||
</div>
|
||||
<template #footer>
|
||||
<VButton type="secondary" @click="handleConfirm">
|
||||
确定
|
||||
{{ $t("core.common.buttons.confirm") }}
|
||||
<span v-if="selected.length">
|
||||
(已选择 {{ selected.length }} 项)
|
||||
{{
|
||||
$t("core.attachment.select_modal.operations.select.result", {
|
||||
count: selected.length,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</VButton>
|
||||
</template>
|
||||
|
|
|
@ -114,17 +114,24 @@ watch(
|
|||
:body-class="['!p-0']"
|
||||
:visible="visible"
|
||||
:width="650"
|
||||
title="上传附件"
|
||||
:title="$t('core.attachment.upload_modal.title')"
|
||||
@update:visible="onVisibleChange"
|
||||
>
|
||||
<div class="w-full p-4">
|
||||
<div class="mb-2">
|
||||
<span class="text-sm text-gray-800">选择分组:</span>
|
||||
<span class="text-sm text-gray-800">
|
||||
{{ $t("core.attachment.upload_modal.filters.group.label") }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mb-3 grid grid-cols-2 gap-x-2 gap-y-3 sm:grid-cols-4">
|
||||
<div
|
||||
v-for="(group, index) in [
|
||||
{ metadata: { name: '' }, spec: { displayName: '未分组' } },
|
||||
{
|
||||
metadata: { name: '' },
|
||||
spec: {
|
||||
displayName: $t('core.attachment.common.text.ungrouped'),
|
||||
},
|
||||
},
|
||||
...(groups || []),
|
||||
]"
|
||||
:key="index"
|
||||
|
@ -143,7 +150,9 @@ watch(
|
|||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<span class="text-sm text-gray-800">选择存储策略:</span>
|
||||
<span class="text-sm text-gray-800">
|
||||
{{ $t("core.attachment.upload_modal.filters.policy.label") }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mb-3 grid grid-cols-2 gap-x-2 gap-y-3 sm:grid-cols-4">
|
||||
<div
|
||||
|
@ -171,7 +180,9 @@ watch(
|
|||
class="flex h-full cursor-pointer items-center rounded-base bg-gray-100 p-2 text-gray-500 transition-all hover:bg-gray-200 hover:text-gray-900 hover:shadow-sm"
|
||||
>
|
||||
<div class="flex flex-1 items-center truncate">
|
||||
<span class="truncate text-sm">新建策略</span>
|
||||
<span class="truncate text-sm">
|
||||
{{ $t("core.common.buttons.new") }}
|
||||
</span>
|
||||
</div>
|
||||
<IconAddCircle />
|
||||
</div>
|
||||
|
@ -196,8 +207,10 @@ watch(
|
|||
</div>
|
||||
<div v-if="!policies?.length" class="mb-3">
|
||||
<VAlert
|
||||
title="没有存储策略"
|
||||
description="在上传之前,需要新建一个存储策略"
|
||||
:title="$t('core.attachment.upload_modal.filters.policy.empty.title')"
|
||||
:description="
|
||||
$t('core.attachment.upload_modal.filters.policy.empty.description')
|
||||
"
|
||||
:closable="false"
|
||||
/>
|
||||
</div>
|
||||
|
@ -210,7 +223,11 @@ watch(
|
|||
groupName: selectedGroupName,
|
||||
}"
|
||||
:allowed-meta-fields="['policyName', 'groupName']"
|
||||
:note="selectedPolicyName ? '' : '请先选择存储策略'"
|
||||
:note="
|
||||
selectedPolicyName
|
||||
? ''
|
||||
: $t('core.attachment.upload_modal.filters.policy.not_select')
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</VModal>
|
||||
|
|
|
@ -78,22 +78,24 @@ const handleOpenDetail = (attachment: Attachment) => {
|
|||
<template #icon>
|
||||
<IconUpload class="h-full w-full" />
|
||||
</template>
|
||||
上传
|
||||
{{ $t("core.common.buttons.upload") }}
|
||||
</VButton>
|
||||
</div>
|
||||
<VEmpty
|
||||
v-if="!attachments?.length && !isLoading"
|
||||
message="当前没有附件,你可以尝试刷新或者上传附件"
|
||||
title="当前没有附件"
|
||||
:message="$t('core.attachment.empty.message')"
|
||||
:title="$t('core.attachment.empty.title')"
|
||||
>
|
||||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton @click="handleFetchAttachments">刷新</VButton>
|
||||
<VButton @click="handleFetchAttachments">
|
||||
{{ $t("core.common.buttons.refresh") }}
|
||||
</VButton>
|
||||
<VButton type="secondary" @click="uploadVisible = true">
|
||||
<template #icon>
|
||||
<IconUpload class="h-full w-full" />
|
||||
</template>
|
||||
上传附件
|
||||
{{ $t("core.attachment.empty.actions.upload") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
|
@ -126,12 +128,16 @@ const handleOpenDetail = (attachment: Attachment) => {
|
|||
>
|
||||
<template #loading>
|
||||
<div class="flex h-full items-center justify-center object-cover">
|
||||
<span class="text-xs text-gray-400">加载中...</span>
|
||||
<span class="text-xs text-gray-400">
|
||||
{{ $t("core.common.status.loading") }}...
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #error>
|
||||
<div class="flex h-full items-center justify-center object-cover">
|
||||
<span class="text-xs text-red-400">加载异常</span>
|
||||
<span class="text-xs text-red-400">
|
||||
{{ $t("core.common.status.loading_error") }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</LazyImage>
|
||||
|
@ -168,6 +174,8 @@ const handleOpenDetail = (attachment: Attachment) => {
|
|||
<VPagination
|
||||
v-model:page="page"
|
||||
v-model:size="size"
|
||||
:page-label="$t('core.components.pagination.page_label')"
|
||||
:size-label="$t('core.components.pagination.size_label')"
|
||||
:total="total"
|
||||
:size-options="[60, 120, 200]"
|
||||
/>
|
||||
|
|
|
@ -6,6 +6,7 @@ import { apiClient } from "@/utils/api-client";
|
|||
import { Dialog, Toast } from "@halo-dev/components";
|
||||
import type { Content, Editor } from "@halo-dev/richtext-editor";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
interface useAttachmentControlReturn {
|
||||
attachments: Ref<Attachment[] | undefined>;
|
||||
|
@ -39,6 +40,8 @@ export function useAttachmentControl(filterOptions: {
|
|||
page: Ref<number>;
|
||||
size: Ref<number>;
|
||||
}): useAttachmentControlReturn {
|
||||
const { t } = useI18n();
|
||||
|
||||
const { user, policy, group, keyword, sort, page, size } = filterOptions;
|
||||
|
||||
const selectedAttachment = ref<Attachment>();
|
||||
|
@ -124,9 +127,11 @@ export function useAttachmentControl(filterOptions: {
|
|||
|
||||
const handleDelete = (attachment: Attachment) => {
|
||||
Dialog.warning({
|
||||
title: "确定要删除该附件吗?",
|
||||
description: "删除之后将无法恢复",
|
||||
title: t("core.attachment.operations.delete.title"),
|
||||
description: t("core.common.dialog.descriptions.cannot_be_recovered"),
|
||||
confirmType: "danger",
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await apiClient.extension.storage.attachment.deletestorageHaloRunV1alpha1Attachment(
|
||||
|
@ -141,7 +146,7 @@ export function useAttachmentControl(filterOptions: {
|
|||
}
|
||||
selectedAttachments.value.delete(attachment);
|
||||
|
||||
Toast.success("删除成功");
|
||||
Toast.success(t("core.common.toast.delete_success"));
|
||||
} catch (e) {
|
||||
console.error("Failed to delete attachment", e);
|
||||
} finally {
|
||||
|
@ -153,9 +158,11 @@ export function useAttachmentControl(filterOptions: {
|
|||
|
||||
const handleDeleteInBatch = () => {
|
||||
Dialog.warning({
|
||||
title: "确定要删除所选的附件吗?",
|
||||
description: "删除之后将无法恢复",
|
||||
title: t("core.attachment.operations.delete_in_batch.title"),
|
||||
description: t("core.common.dialog.descriptions.cannot_be_recovered"),
|
||||
confirmType: "danger",
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
const promises = Array.from(selectedAttachments.value).map(
|
||||
|
@ -170,7 +177,7 @@ export function useAttachmentControl(filterOptions: {
|
|||
await Promise.all(promises);
|
||||
selectedAttachments.value.clear();
|
||||
|
||||
Toast.success("删除成功");
|
||||
Toast.success(t("core.common.toast.delete_success"));
|
||||
} catch (e) {
|
||||
console.error("Failed to delete attachments", e);
|
||||
} finally {
|
||||
|
|
|
@ -19,10 +19,10 @@ export default definePlugin({
|
|||
name: "Attachments",
|
||||
component: AttachmentList,
|
||||
meta: {
|
||||
title: "附件",
|
||||
title: "core.attachment.title",
|
||||
permissions: ["system:attachments:view"],
|
||||
menu: {
|
||||
name: "附件",
|
||||
name: "core.sidebar.menu.items.attachments",
|
||||
group: "content",
|
||||
icon: markRaw(IconFolder),
|
||||
priority: 3,
|
||||
|
|
|
@ -22,6 +22,9 @@ import FilterTag from "@/components/filter/FilterTag.vue";
|
|||
import FilterCleanButton from "@/components/filter/FilterCleanButton.vue";
|
||||
import { getNode } from "@formkit/core";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const checkAll = ref(false);
|
||||
const selectedComment = ref<ListedComment>();
|
||||
|
@ -31,15 +34,15 @@ const keyword = ref("");
|
|||
// Filters
|
||||
const ApprovedFilterItems: { label: string; value?: boolean }[] = [
|
||||
{
|
||||
label: "全部",
|
||||
label: t("core.comment.filters.status.items.all"),
|
||||
value: undefined,
|
||||
},
|
||||
{
|
||||
label: "已审核",
|
||||
label: t("core.comment.filters.status.items.approved"),
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
label: "待审核",
|
||||
label: t("core.comment.filters.status.items.pending_review"),
|
||||
value: false,
|
||||
},
|
||||
];
|
||||
|
@ -51,19 +54,19 @@ const SortFilterItems: {
|
|||
value?: Sort;
|
||||
}[] = [
|
||||
{
|
||||
label: "默认",
|
||||
label: t("core.comment.filters.sort.items.default"),
|
||||
value: undefined,
|
||||
},
|
||||
{
|
||||
label: "最后回复时间",
|
||||
label: t("core.comment.filters.sort.items.last_reply_time"),
|
||||
value: "LAST_REPLY_TIME",
|
||||
},
|
||||
{
|
||||
label: "回复数",
|
||||
label: t("core.comment.filters.sort.items.reply_count"),
|
||||
value: "REPLY_COUNT",
|
||||
},
|
||||
{
|
||||
label: "创建时间",
|
||||
label: t("core.comment.filters.sort.items.creation_time"),
|
||||
value: "CREATE_TIME",
|
||||
},
|
||||
];
|
||||
|
@ -205,9 +208,13 @@ watch(
|
|||
|
||||
const handleDeleteInBatch = async () => {
|
||||
Dialog.warning({
|
||||
title: "确定要删除所选的评论吗?",
|
||||
description: "将同时删除所有评论下的回复,该操作不可恢复。",
|
||||
title: t("core.comment.operations.delete_comment_in_batch.title"),
|
||||
description: t(
|
||||
"core.comment.operations.delete_comment_in_batch.description"
|
||||
),
|
||||
confirmType: "danger",
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
const promises = selectedCommentNames.value.map((name) => {
|
||||
|
@ -220,7 +227,7 @@ const handleDeleteInBatch = async () => {
|
|||
await Promise.all(promises);
|
||||
selectedCommentNames.value = [];
|
||||
|
||||
Toast.success("删除成功");
|
||||
Toast.success(t("core.common.toast.delete_success"));
|
||||
} catch (e) {
|
||||
console.error("Failed to delete comments", e);
|
||||
} finally {
|
||||
|
@ -232,7 +239,9 @@ const handleDeleteInBatch = async () => {
|
|||
|
||||
const handleApproveInBatch = async () => {
|
||||
Dialog.warning({
|
||||
title: "确定要审核通过所选的评论吗?",
|
||||
title: t("core.comment.operations.approve_comment_in_batch.title"),
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
const commentsToUpdate = comments.value?.filter((comment) => {
|
||||
|
@ -262,7 +271,7 @@ const handleApproveInBatch = async () => {
|
|||
await Promise.all(promises || []);
|
||||
selectedCommentNames.value = [];
|
||||
|
||||
Toast.success("操作成功");
|
||||
Toast.success(t("core.common.toast.operation_success"));
|
||||
} catch (e) {
|
||||
console.error("Failed to approve comments in batch", e);
|
||||
} finally {
|
||||
|
@ -273,7 +282,7 @@ const handleApproveInBatch = async () => {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<VPageHeader title="评论">
|
||||
<VPageHeader :title="$t('core.comment.title')">
|
||||
<template #icon>
|
||||
<IconMessage class="mr-2 self-center" />
|
||||
</template>
|
||||
|
@ -305,7 +314,7 @@ const handleApproveInBatch = async () => {
|
|||
<FormKit
|
||||
id="keywordInput"
|
||||
outer-class="!p-0"
|
||||
placeholder="输入关键词搜索"
|
||||
:placeholder="$t('core.common.placeholder.search')"
|
||||
type="text"
|
||||
name="keyword"
|
||||
:model-value="keyword"
|
||||
|
@ -313,7 +322,11 @@ const handleApproveInBatch = async () => {
|
|||
></FormKit>
|
||||
|
||||
<FilterTag v-if="keyword" @close="handleClearKeyword()">
|
||||
关键词:{{ keyword }}
|
||||
{{
|
||||
$t("core.common.filters.results.keyword", {
|
||||
keyword: keyword,
|
||||
})
|
||||
}}
|
||||
</FilterTag>
|
||||
|
||||
<FilterTag
|
||||
|
@ -322,21 +335,33 @@ const handleApproveInBatch = async () => {
|
|||
handleApprovedFilterItemChange(ApprovedFilterItems[0])
|
||||
"
|
||||
>
|
||||
状态:{{ selectedApprovedFilterItem.label }}
|
||||
{{
|
||||
$t("core.common.filters.results.status", {
|
||||
status: selectedApprovedFilterItem.label,
|
||||
})
|
||||
}}
|
||||
</FilterTag>
|
||||
|
||||
<FilterTag
|
||||
v-if="selectedUser"
|
||||
@close="handleSelectUser(undefined)"
|
||||
>
|
||||
评论者:{{ selectedUser?.spec.displayName }}
|
||||
{{
|
||||
$t("core.comment.filters.owner.result", {
|
||||
owner: selectedUser.spec.displayName,
|
||||
})
|
||||
}}
|
||||
</FilterTag>
|
||||
|
||||
<FilterTag
|
||||
v-if="selectedSortFilterItem.value != undefined"
|
||||
@close="handleSortFilterItemChange(SortFilterItems[0])"
|
||||
>
|
||||
排序:{{ selectedSortFilterItem.label }}
|
||||
{{
|
||||
$t("core.common.filters.results.sort", {
|
||||
sort: selectedSortFilterItem.label,
|
||||
})
|
||||
}}
|
||||
</FilterTag>
|
||||
|
||||
<FilterCleanButton
|
||||
|
@ -346,10 +371,14 @@ const handleApproveInBatch = async () => {
|
|||
</div>
|
||||
<VSpace v-else>
|
||||
<VButton type="secondary" @click="handleApproveInBatch">
|
||||
审核通过
|
||||
{{
|
||||
$t(
|
||||
"core.comment.operations.approve_comment_in_batch.button"
|
||||
)
|
||||
}}
|
||||
</VButton>
|
||||
<VButton type="danger" @click="handleDeleteInBatch">
|
||||
删除
|
||||
{{ $t("core.common.buttons.delete") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</div>
|
||||
|
@ -359,7 +388,9 @@ const handleApproveInBatch = async () => {
|
|||
<div
|
||||
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
||||
>
|
||||
<span class="mr-0.5"> 状态 </span>
|
||||
<span class="mr-0.5">
|
||||
{{ $t("core.common.filters.labels.status") }}
|
||||
</span>
|
||||
<span>
|
||||
<IconArrowDown />
|
||||
</span>
|
||||
|
@ -394,7 +425,9 @@ const handleApproveInBatch = async () => {
|
|||
<div
|
||||
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
||||
>
|
||||
<span class="mr-0.5">评论者</span>
|
||||
<span class="mr-0.5">
|
||||
{{ $t("core.comment.filters.owner.label") }}
|
||||
</span>
|
||||
<span>
|
||||
<IconArrowDown />
|
||||
</span>
|
||||
|
@ -404,7 +437,9 @@ const handleApproveInBatch = async () => {
|
|||
<div
|
||||
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
||||
>
|
||||
<span class="mr-0.5"> 排序 </span>
|
||||
<span class="mr-0.5">
|
||||
{{ $t("core.common.filters.labels.sort") }}
|
||||
</span>
|
||||
<span>
|
||||
<IconArrowDown />
|
||||
</span>
|
||||
|
@ -437,7 +472,7 @@ const handleApproveInBatch = async () => {
|
|||
@click="refetch()"
|
||||
>
|
||||
<IconRefreshLine
|
||||
v-tooltip="`刷新`"
|
||||
v-tooltip="$t('core.common.buttons.refresh')"
|
||||
:class="{ 'animate-spin text-gray-900': isFetching }"
|
||||
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
|
||||
/>
|
||||
|
@ -450,10 +485,15 @@ const handleApproveInBatch = async () => {
|
|||
</template>
|
||||
<VLoading v-if="isLoading" />
|
||||
<Transition v-else-if="!comments?.length" appear name="fade">
|
||||
<VEmpty message="你可以尝试刷新或者修改筛选条件" title="当前没有评论">
|
||||
<VEmpty
|
||||
:message="$t('core.comment.empty.message')"
|
||||
:title="$t('core.comment.empty.title')"
|
||||
>
|
||||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton @click="refetch">刷新</VButton>
|
||||
<VButton @click="refetch">
|
||||
{{ $t("core.common.buttons.refresh") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
</VEmpty>
|
||||
|
@ -488,6 +528,8 @@ const handleApproveInBatch = async () => {
|
|||
<VPagination
|
||||
v-model:page="page"
|
||||
v-model:size="size"
|
||||
:page-label="$t('core.components.pagination.page_label')"
|
||||
:size-label="$t('core.components.pagination.size_label')"
|
||||
:total="total"
|
||||
:size-options="[20, 30, 50, 100]"
|
||||
/>
|
||||
|
|
|
@ -29,8 +29,10 @@ import type { RouteLocationRaw } from "vue-router";
|
|||
import cloneDeep from "lodash.clonedeep";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -56,16 +58,18 @@ provide<Ref<ListedReply | undefined>>("hoveredReply", hoveredReply);
|
|||
|
||||
const handleDelete = async () => {
|
||||
Dialog.warning({
|
||||
title: "确认要删除该评论吗?",
|
||||
description: "删除评论的同时会删除该评论下的所有回复,该操作不可恢复。",
|
||||
title: t("core.comment.operations.delete_comment.title"),
|
||||
description: t("core.comment.operations.delete_comment.description"),
|
||||
confirmType: "danger",
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await apiClient.extension.comment.deletecontentHaloRunV1alpha1Comment({
|
||||
name: props.comment?.comment?.metadata.name as string,
|
||||
});
|
||||
|
||||
Toast.success("删除成功");
|
||||
Toast.success(t("core.common.toast.delete_success"));
|
||||
} catch (error) {
|
||||
console.error("Failed to delete comment", error);
|
||||
} finally {
|
||||
|
@ -77,7 +81,9 @@ const handleDelete = async () => {
|
|||
|
||||
const handleApproveReplyInBatch = async () => {
|
||||
Dialog.warning({
|
||||
title: "确定要审核通过该评论的所有回复吗?",
|
||||
title: t("core.comment.operations.approve_applies_in_batch.title"),
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
const repliesToUpdate = replies.value?.filter((reply) => {
|
||||
|
@ -99,7 +105,7 @@ const handleApproveReplyInBatch = async () => {
|
|||
});
|
||||
await Promise.all(promises || []);
|
||||
|
||||
Toast.success("操作成功");
|
||||
Toast.success(t("core.common.toast.operation_success"));
|
||||
} catch (e) {
|
||||
console.error("Failed to approve comment replies in batch", e);
|
||||
} finally {
|
||||
|
@ -120,7 +126,7 @@ const handleApprove = async () => {
|
|||
comment: commentToUpdate,
|
||||
});
|
||||
|
||||
Toast.success("操作成功");
|
||||
Toast.success(t("core.common.toast.operation_success"));
|
||||
} catch (error) {
|
||||
console.error("Failed to approve comment", error);
|
||||
} finally {
|
||||
|
@ -202,7 +208,7 @@ const SubjectRefProvider = ref<
|
|||
Post: (subject: Extension): SubjectRefResult => {
|
||||
const post = subject as Post;
|
||||
return {
|
||||
label: "文章",
|
||||
label: t("core.comment.subject_refs.post"),
|
||||
title: post.spec.title,
|
||||
externalUrl: post.status?.permalink,
|
||||
route: {
|
||||
|
@ -218,7 +224,7 @@ const SubjectRefProvider = ref<
|
|||
SinglePage: (subject: Extension): SubjectRefResult => {
|
||||
const singlePage = subject as SinglePage;
|
||||
return {
|
||||
label: "单页",
|
||||
label: t("core.comment.subject_refs.page"),
|
||||
title: singlePage.spec.title,
|
||||
externalUrl: singlePage.status?.permalink,
|
||||
route: {
|
||||
|
@ -236,8 +242,8 @@ const subjectRefResult = computed(() => {
|
|||
const { subject } = props.comment;
|
||||
if (!subject) {
|
||||
return {
|
||||
label: "未知",
|
||||
title: "未知",
|
||||
label: t("core.comment.subject_refs.unknown"),
|
||||
title: t("core.comment.subject_refs.unknown"),
|
||||
};
|
||||
}
|
||||
const subjectRef = SubjectRefProvider.value.find((provider) =>
|
||||
|
@ -245,8 +251,8 @@ const subjectRefResult = computed(() => {
|
|||
);
|
||||
if (!subjectRef) {
|
||||
return {
|
||||
label: "未知",
|
||||
title: "未知",
|
||||
label: t("core.comment.subject_refs.unknown"),
|
||||
title: t("core.comment.subject_refs.unknown"),
|
||||
};
|
||||
}
|
||||
return subjectRef[subject.kind](subject);
|
||||
|
@ -301,11 +307,15 @@ const subjectRefResult = computed(() => {
|
|||
class="select-none text-gray-700 hover:text-gray-900"
|
||||
@click="handleToggleShowReplies"
|
||||
>
|
||||
{{ comment?.comment?.status?.replyCount || 0 }} 条回复
|
||||
{{
|
||||
$t("core.comment.list.fields.reply_count", {
|
||||
count: comment?.comment?.status?.replyCount || 0,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<VStatusDot
|
||||
v-if="comment?.comment?.status?.unreadReplyCount || 0 > 0"
|
||||
v-tooltip="`有新的回复`"
|
||||
v-tooltip="$t('core.comment.list.fields.has_new_replies')"
|
||||
state="success"
|
||||
animate
|
||||
/>
|
||||
|
@ -313,7 +323,7 @@ const subjectRefResult = computed(() => {
|
|||
class="select-none text-gray-700 hover:text-gray-900"
|
||||
@click="handleTriggerReply"
|
||||
>
|
||||
回复
|
||||
{{ $t("core.comment.operations.reply.button") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -341,14 +351,20 @@ const subjectRefResult = computed(() => {
|
|||
<template #description>
|
||||
<VStatusDot state="success">
|
||||
<template #text>
|
||||
<span class="text-xs text-gray-500">待审核</span>
|
||||
<span class="text-xs text-gray-500">
|
||||
{{ $t("core.comment.list.fields.pending_review") }}
|
||||
</span>
|
||||
</template>
|
||||
</VStatusDot>
|
||||
</template>
|
||||
</VEntityField>
|
||||
<VEntityField v-if="comment?.comment?.metadata.deletionTimestamp">
|
||||
<template #description>
|
||||
<VStatusDot v-tooltip="`删除中`" state="warning" animate />
|
||||
<VStatusDot
|
||||
v-tooltip="$t('core.common.status.deleting')"
|
||||
state="warning"
|
||||
animate
|
||||
/>
|
||||
</template>
|
||||
</VEntityField>
|
||||
<VEntityField>
|
||||
|
@ -375,7 +391,7 @@ const subjectRefResult = computed(() => {
|
|||
block
|
||||
@click="handleApprove"
|
||||
>
|
||||
审核通过
|
||||
{{ $t("core.comment.operations.approve_comment_in_batch.button") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-close-popper
|
||||
|
@ -383,10 +399,10 @@ const subjectRefResult = computed(() => {
|
|||
block
|
||||
@click="handleApproveReplyInBatch"
|
||||
>
|
||||
审核通过所有回复
|
||||
{{ $t("core.comment.operations.approve_applies_in_batch.button") }}
|
||||
</VButton>
|
||||
<VButton v-close-popper block type="danger" @click="handleDelete">
|
||||
删除
|
||||
{{ $t("core.common.buttons.delete") }}
|
||||
</VButton>
|
||||
</template>
|
||||
|
||||
|
@ -397,15 +413,20 @@ const subjectRefResult = computed(() => {
|
|||
>
|
||||
<VLoading v-if="isLoading" />
|
||||
<Transition v-else-if="!replies?.length" appear name="fade">
|
||||
<VEmpty message="你可以尝试刷新或者创建新回复" title="当前没有回复">
|
||||
<VEmpty
|
||||
:message="$t('core.comment.reply_empty.message')"
|
||||
:title="$t('core.comment.reply_empty.title')"
|
||||
>
|
||||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton @click="refetch()">刷新</VButton>
|
||||
<VButton @click="refetch()">
|
||||
{{ $t("core.common.buttons.refresh") }}
|
||||
</VButton>
|
||||
<VButton type="secondary" @click="replyModal = true">
|
||||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
创建新回复
|
||||
{{ $t("core.comment.reply_empty.new") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
|
|
|
@ -20,6 +20,9 @@ import { reset } from "@formkit/core";
|
|||
import cloneDeep from "lodash.clonedeep";
|
||||
import { setFocus } from "@/formkit/utils/focus";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -106,7 +109,9 @@ const handleCreateReply = async () => {
|
|||
});
|
||||
onVisibleChange(false);
|
||||
|
||||
Toast.success("回复成功");
|
||||
Toast.success(
|
||||
t("core.comment.reply_modal.operations.submit.toast_success")
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to create comment reply", error);
|
||||
} finally {
|
||||
|
@ -145,7 +150,7 @@ watchEffect(() => {
|
|||
|
||||
<template>
|
||||
<VModal
|
||||
title="回复"
|
||||
:title="$t('core.comment.reply_modal.title')"
|
||||
:visible="visible"
|
||||
:width="500"
|
||||
@update:visible="onVisibleChange"
|
||||
|
@ -161,7 +166,7 @@ watchEffect(() => {
|
|||
:id="contentInputId"
|
||||
v-model="formState.raw"
|
||||
type="textarea"
|
||||
validation-label="内容"
|
||||
:validation-label="$t('core.comment.reply_modal.fields.content.label')"
|
||||
:rows="6"
|
||||
validation="required|length:0,1024"
|
||||
></FormKit>
|
||||
|
@ -182,10 +187,13 @@ watchEffect(() => {
|
|||
v-if="visible"
|
||||
:loading="saving"
|
||||
type="secondary"
|
||||
:text="$t('core.common.buttons.submit')"
|
||||
@submit="$formkit.submit(formId)"
|
||||
>
|
||||
</SubmitButton>
|
||||
<VButton @click="onVisibleChange(false)">取消 Esc</VButton>
|
||||
<VButton @click="onVisibleChange(false)">
|
||||
{{ $t("core.common.buttons.cancel_and_shortcut") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
</VModal>
|
||||
|
|
|
@ -15,6 +15,9 @@ import { formatDatetime } from "@/utils/date";
|
|||
import { apiClient } from "@/utils/api-client";
|
||||
import { computed, inject, type Ref } from "vue";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -46,16 +49,18 @@ const quoteReply = computed(() => {
|
|||
|
||||
const handleDelete = async () => {
|
||||
Dialog.warning({
|
||||
title: "确认要删除该回复吗?",
|
||||
description: "该操作不可恢复。",
|
||||
title: t("core.comment.operations.delete_reply.title"),
|
||||
description: t("core.common.dialog.descriptions.cannot_be_recovered"),
|
||||
confirmType: "danger",
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await apiClient.extension.reply.deletecontentHaloRunV1alpha1Reply({
|
||||
name: props.reply?.reply.metadata.name as string,
|
||||
});
|
||||
|
||||
Toast.success("删除成功");
|
||||
Toast.success(t("core.common.toast.delete_success"));
|
||||
} catch (error) {
|
||||
console.error("Failed to delete comment reply", error);
|
||||
} finally {
|
||||
|
@ -76,7 +81,7 @@ const handleApprove = async () => {
|
|||
reply: replyToUpdate,
|
||||
});
|
||||
|
||||
Toast.success("操作成功");
|
||||
Toast.success(t("core.common.toast.operation_success"));
|
||||
} catch (error) {
|
||||
console.error("Failed to approve comment reply", error);
|
||||
} finally {
|
||||
|
@ -146,7 +151,7 @@ const isHoveredReply = computed(() => {
|
|||
class="select-none text-gray-700 hover:text-gray-900"
|
||||
@click="handleTriggerReply"
|
||||
>
|
||||
回复
|
||||
{{ $t("core.comment.operations.reply.button") }}
|
||||
</span>
|
||||
<div v-if="false" class="flex items-center">
|
||||
<VTag>New</VTag>
|
||||
|
@ -161,14 +166,20 @@ const isHoveredReply = computed(() => {
|
|||
<template #description>
|
||||
<VStatusDot state="success">
|
||||
<template #text>
|
||||
<span class="text-xs text-gray-500">待审核</span>
|
||||
<span class="text-xs text-gray-500">
|
||||
{{ $t("core.comment.list.fields.pending_review") }}
|
||||
</span>
|
||||
</template>
|
||||
</VStatusDot>
|
||||
</template>
|
||||
</VEntityField>
|
||||
<VEntityField v-if="reply?.reply.metadata.deletionTimestamp">
|
||||
<template #description>
|
||||
<VStatusDot v-tooltip="`删除中`" state="warning" animate />
|
||||
<VStatusDot
|
||||
v-tooltip="$t('core.common.status.deleting')"
|
||||
state="warning"
|
||||
animate
|
||||
/>
|
||||
</template>
|
||||
</VEntityField>
|
||||
<VEntityField>
|
||||
|
@ -193,7 +204,7 @@ const isHoveredReply = computed(() => {
|
|||
block
|
||||
@click="handleApprove"
|
||||
>
|
||||
审核通过
|
||||
{{ $t("core.comment.operations.approve_reply.button") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-permission="['system:comments:manage']"
|
||||
|
@ -202,7 +213,7 @@ const isHoveredReply = computed(() => {
|
|||
type="danger"
|
||||
@click="handleDelete"
|
||||
>
|
||||
删除
|
||||
{{ $t("core.common.buttons.delete") }}
|
||||
</VButton>
|
||||
</template>
|
||||
</VEntity>
|
||||
|
|
|
@ -19,11 +19,11 @@ export default definePlugin({
|
|||
name: "Comments",
|
||||
component: CommentList,
|
||||
meta: {
|
||||
title: "评论",
|
||||
title: "core.comment.title",
|
||||
searchable: true,
|
||||
permissions: ["system:comments:view"],
|
||||
menu: {
|
||||
name: "评论",
|
||||
name: "core.sidebar.menu.items.comments",
|
||||
group: "content",
|
||||
icon: markRaw(IconMessage),
|
||||
priority: 2,
|
||||
|
|
|
@ -16,7 +16,9 @@ const dashboardStats = inject<Ref<DashboardStats>>("dashboardStats");
|
|||
</span>
|
||||
|
||||
<div>
|
||||
<span class="text-sm text-gray-500">评论</span>
|
||||
<span class="text-sm text-gray-500">
|
||||
{{ $t("core.dashboard.widgets.presets.comment_stats.title") }}
|
||||
</span>
|
||||
<p class="text-2xl font-medium text-gray-900">
|
||||
{{ dashboardStats?.approvedComments }}
|
||||
</p>
|
||||
|
|
|
@ -27,8 +27,10 @@ import { usePermission } from "@/utils/permission";
|
|||
import { getNode } from "@formkit/core";
|
||||
import FilterTag from "@/components/filter/FilterTag.vue";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
const { t } = useI18n();
|
||||
|
||||
const selectedPageNames = ref<string[]>([]);
|
||||
const checkedAll = ref(false);
|
||||
|
@ -87,9 +89,11 @@ const handleCheckAllChange = (e: Event) => {
|
|||
|
||||
const handleDeletePermanently = async (singlePage: SinglePage) => {
|
||||
Dialog.warning({
|
||||
title: "确认要永久删除该自定义页面吗?",
|
||||
description: "删除之后将无法恢复",
|
||||
title: t("core.deleted_page.operations.delete.title"),
|
||||
description: t("core.deleted_page.operations.delete.description"),
|
||||
confirmType: "danger",
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
await apiClient.extension.singlePage.deletecontentHaloRunV1alpha1SinglePage(
|
||||
{
|
||||
|
@ -98,16 +102,18 @@ const handleDeletePermanently = async (singlePage: SinglePage) => {
|
|||
);
|
||||
await refetch();
|
||||
|
||||
Toast.success("删除成功");
|
||||
Toast.success(t("core.common.toast.delete_success"));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeletePermanentlyInBatch = async () => {
|
||||
Dialog.warning({
|
||||
title: "确定要确认永久删除选中的自定义页面吗?",
|
||||
description: "删除之后将无法恢复",
|
||||
title: t("core.deleted_page.operations.delete_in_batch.title"),
|
||||
description: t("core.deleted_page.operations.delete_in_batch.description"),
|
||||
confirmType: "danger",
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
await Promise.all(
|
||||
selectedPageNames.value.map((name) => {
|
||||
|
@ -121,15 +127,17 @@ const handleDeletePermanentlyInBatch = async () => {
|
|||
await refetch();
|
||||
selectedPageNames.value = [];
|
||||
|
||||
Toast.success("删除成功");
|
||||
Toast.success(t("core.common.toast.delete_success"));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleRecovery = async (singlePage: SinglePage) => {
|
||||
Dialog.warning({
|
||||
title: "确认要恢复该自定义页面吗?",
|
||||
description: "该操作会将自定义页面恢复到被删除之前的状态",
|
||||
title: t("core.deleted_page.operations.recovery.title"),
|
||||
description: t("core.deleted_page.operations.recovery.description"),
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
const singlePageToUpdate = cloneDeep(singlePage);
|
||||
singlePageToUpdate.spec.deleted = false;
|
||||
|
@ -141,15 +149,19 @@ const handleRecovery = async (singlePage: SinglePage) => {
|
|||
);
|
||||
await refetch();
|
||||
|
||||
Toast.success("恢复成功");
|
||||
Toast.success(t("core.common.toast.recovery_success"));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleRecoveryInBatch = async () => {
|
||||
Dialog.warning({
|
||||
title: "确认要恢复选中的自定义页面吗?",
|
||||
description: "该操作会将自定义页面恢复到被删除之前的状态",
|
||||
title: t("core.deleted_page.operations.recovery_in_batch.title"),
|
||||
description: t(
|
||||
"core.deleted_page.operations.recovery_in_batch.description"
|
||||
),
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
await Promise.all(
|
||||
selectedPageNames.value.map((name) => {
|
||||
|
@ -178,7 +190,7 @@ const handleRecoveryInBatch = async () => {
|
|||
await refetch();
|
||||
selectedPageNames.value = [];
|
||||
|
||||
Toast.success("恢复成功");
|
||||
Toast.success(t("core.common.toast.recovery_success"));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -203,13 +215,15 @@ function handleClearKeyword() {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<VPageHeader title="自定义页面回收站">
|
||||
<VPageHeader :title="$t('core.deleted_page.title')">
|
||||
<template #icon>
|
||||
<IconDeleteBin class="mr-2 self-center text-green-600" />
|
||||
</template>
|
||||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton :route="{ name: 'SinglePages' }" size="sm">返回</VButton>
|
||||
<VButton :route="{ name: 'SinglePages' }" size="sm">
|
||||
{{ $t("core.common.buttons.back") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-permission="['system:singlepages:manage']"
|
||||
:route="{ name: 'SinglePageEditor' }"
|
||||
|
@ -218,7 +232,7 @@ function handleClearKeyword() {
|
|||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
新建
|
||||
{{ $t("core.common.buttons.new") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
|
@ -249,7 +263,7 @@ function handleClearKeyword() {
|
|||
<FormKit
|
||||
id="keywordInput"
|
||||
outer-class="!p-0"
|
||||
placeholder="输入关键词搜索"
|
||||
:placeholder="$t('core.common.placeholder.search')"
|
||||
type="text"
|
||||
name="keyword"
|
||||
:model-value="keyword"
|
||||
|
@ -257,15 +271,19 @@ function handleClearKeyword() {
|
|||
></FormKit>
|
||||
|
||||
<FilterTag v-if="keyword" @close="handleClearKeyword()">
|
||||
关键词:{{ keyword }}
|
||||
{{
|
||||
$t("core.common.filters.results.keyword", {
|
||||
keyword: keyword,
|
||||
})
|
||||
}}
|
||||
</FilterTag>
|
||||
</div>
|
||||
<VSpace v-else>
|
||||
<VButton type="danger" @click="handleDeletePermanentlyInBatch">
|
||||
永久删除
|
||||
{{ $t("core.common.buttons.delete_permanently") }}
|
||||
</VButton>
|
||||
<VButton type="default" @click="handleRecoveryInBatch">
|
||||
恢复
|
||||
{{ $t("core.common.buttons.recovery") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</div>
|
||||
|
@ -290,18 +308,20 @@ function handleClearKeyword() {
|
|||
<VLoading v-if="isLoading" />
|
||||
<Transition v-else-if="!singlePages?.length" appear name="fade">
|
||||
<VEmpty
|
||||
message="你可以尝试刷新或者返回自定义页面管理"
|
||||
title="没有自定义页面被放入回收站"
|
||||
:message="$t('core.deleted_page.empty.message')"
|
||||
:title="$t('core.deleted_page.empty.title')"
|
||||
>
|
||||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton @click="refetch">刷新</VButton>
|
||||
<VButton @click="refetch">
|
||||
{{ $t("core.common.buttons.refresh") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-permission="['system:singlepages:view']"
|
||||
:route="{ name: 'SinglePages' }"
|
||||
type="primary"
|
||||
>
|
||||
返回
|
||||
{{ $t("core.common.buttons.back") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
|
@ -330,10 +350,18 @@ function handleClearKeyword() {
|
|||
<template #description>
|
||||
<VSpace>
|
||||
<span class="text-xs text-gray-500">
|
||||
访问量 {{ singlePage.stats.visit || 0 }}
|
||||
{{
|
||||
$t("core.page.list.fields.visits", {
|
||||
visits: singlePage.stats.visit || 0,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500">
|
||||
评论 {{ singlePage.stats.totalComment || 0 }}
|
||||
{{
|
||||
$t("core.page.list.fields.comments", {
|
||||
comments: singlePage.stats.totalComment || 0,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</VSpace>
|
||||
</template>
|
||||
|
@ -365,14 +393,22 @@ function handleClearKeyword() {
|
|||
</VEntityField>
|
||||
<VEntityField v-if="!singlePage?.page?.spec.deleted">
|
||||
<template #description>
|
||||
<VStatusDot v-tooltip="`恢复中`" state="success" animate />
|
||||
<VStatusDot
|
||||
v-tooltip="$t('core.common.tooltips.recovering')"
|
||||
state="success"
|
||||
animate
|
||||
/>
|
||||
</template>
|
||||
</VEntityField>
|
||||
<VEntityField
|
||||
v-if="singlePage?.page?.metadata.deletionTimestamp"
|
||||
>
|
||||
<template #description>
|
||||
<VStatusDot v-tooltip="`删除中`" state="warning" animate />
|
||||
<VStatusDot
|
||||
v-tooltip="$t('core.common.status.deleting')"
|
||||
state="warning"
|
||||
animate
|
||||
/>
|
||||
</template>
|
||||
</VEntityField>
|
||||
<VEntityField>
|
||||
|
@ -393,7 +429,7 @@ function handleClearKeyword() {
|
|||
type="danger"
|
||||
@click="handleDeletePermanently(singlePage.page)"
|
||||
>
|
||||
永久删除
|
||||
{{ $t("core.common.buttons.delete_permanently") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-close-popper
|
||||
|
@ -401,7 +437,7 @@ function handleClearKeyword() {
|
|||
type="default"
|
||||
@click="handleRecovery(singlePage.page)"
|
||||
>
|
||||
恢复
|
||||
{{ $t("core.common.buttons.recovery") }}
|
||||
</VButton>
|
||||
</template>
|
||||
</VEntity>
|
||||
|
@ -414,6 +450,8 @@ function handleClearKeyword() {
|
|||
<VPagination
|
||||
v-model:page="page"
|
||||
v-model:size="size"
|
||||
:page-label="$t('core.components.pagination.page_label')"
|
||||
:size-label="$t('core.components.pagination.size_label')"
|
||||
:total="total"
|
||||
:size-options="[20, 30, 50, 100]"
|
||||
/>
|
||||
|
|
|
@ -11,7 +11,6 @@ import {
|
|||
Dialog,
|
||||
} from "@halo-dev/components";
|
||||
import SinglePageSettingModal from "./components/SinglePageSettingModal.vue";
|
||||
import PostPreviewModal from "../posts/components/PostPreviewModal.vue";
|
||||
import type { SinglePage, SinglePageRequest } from "@halo-dev/api-client";
|
||||
import {
|
||||
computed,
|
||||
|
@ -34,8 +33,10 @@ import {
|
|||
} from "@/composables/use-editor-extension-points";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import EditorProviderSelector from "@/components/dropdown-selector/EditorProviderSelector.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
||||
// Editor providers
|
||||
const { editorProviders } = useEditorExtensionPoints();
|
||||
|
@ -90,7 +91,6 @@ const formState = ref<SinglePageRequest>(cloneDeep(initialFormState));
|
|||
const saving = ref(false);
|
||||
const publishing = ref(false);
|
||||
const settingModal = ref(false);
|
||||
const previewModal = ref(false);
|
||||
|
||||
const isUpdateMode = computed(() => {
|
||||
return !!formState.value.page.metadata.creationTimestamp;
|
||||
|
@ -118,7 +118,7 @@ const handleSave = async () => {
|
|||
|
||||
//Set default title and slug
|
||||
if (!formState.value.page.spec.title) {
|
||||
formState.value.page.spec.title = "无标题页面";
|
||||
formState.value.page.spec.title = t("core.page_editor.untitled");
|
||||
}
|
||||
if (!formState.value.page.spec.slug) {
|
||||
formState.value.page.spec.slug = new Date().getTime().toString();
|
||||
|
@ -139,13 +139,13 @@ const handleSave = async () => {
|
|||
routeQueryName.value = data.metadata.name;
|
||||
}
|
||||
|
||||
Toast.success("保存成功");
|
||||
Toast.success(t("core.common.toast.save_success"));
|
||||
|
||||
handleClearCache(routeQueryName.value as string);
|
||||
await handleFetchContent();
|
||||
} catch (error) {
|
||||
console.error("Failed to save single page", error);
|
||||
Toast.error("保存失败,请重试");
|
||||
Toast.error(t("core.common.toast.save_failed_and_retry"));
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
|
@ -183,11 +183,11 @@ const handlePublish = async () => {
|
|||
router.push({ name: "SinglePages" });
|
||||
}
|
||||
|
||||
Toast.success("发布成功");
|
||||
Toast.success(t("core.common.toast.publish_success"));
|
||||
handleClearCache(routeQueryName.value as string);
|
||||
} catch (error) {
|
||||
console.error("Failed to publish single page", error);
|
||||
Toast.error("发布失败,请重试");
|
||||
Toast.error(t("core.common.toast.publish_failed_and_retry"));
|
||||
} finally {
|
||||
publishing.value = false;
|
||||
}
|
||||
|
@ -243,8 +243,12 @@ const handleFetchContent = async () => {
|
|||
formState.value.page = data;
|
||||
} else {
|
||||
Dialog.warning({
|
||||
title: "警告",
|
||||
description: `未找到符合 ${data.rawType} 格式的编辑器,请检查是否已安装编辑器插件`,
|
||||
title: t("core.common.dialog.titles.warning"),
|
||||
description: t("core.common.dialog.descriptions.editor_not_found", {
|
||||
raw_type: data.rawType,
|
||||
}),
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: () => {
|
||||
router.back();
|
||||
},
|
||||
|
@ -330,8 +334,7 @@ const { handleSetContentCache, handleResetCache, handleClearCache } =
|
|||
@saved="onSettingSaved"
|
||||
@published="onSettingPublished"
|
||||
/>
|
||||
<PostPreviewModal v-model:visible="previewModal" />
|
||||
<VPageHeader title="自定义页面">
|
||||
<VPageHeader :title="$t('core.page.title')">
|
||||
<template #icon>
|
||||
<IconPages class="mr-2 self-center" />
|
||||
</template>
|
||||
|
@ -342,21 +345,11 @@ const { handleSetContentCache, handleResetCache, handleClearCache } =
|
|||
:provider="currentEditorProvider"
|
||||
@select="handleChangeEditorProvider"
|
||||
/>
|
||||
|
||||
<!-- TODO: add preview single page support -->
|
||||
<VButton
|
||||
v-if="false"
|
||||
size="sm"
|
||||
type="default"
|
||||
@click="previewModal = true"
|
||||
>
|
||||
预览
|
||||
</VButton>
|
||||
<VButton :loading="saving" size="sm" type="default" @click="handleSave">
|
||||
<template #icon>
|
||||
<IconSave class="h-full w-full" />
|
||||
</template>
|
||||
保存
|
||||
{{ $t("core.common.buttons.save") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-if="isUpdateMode"
|
||||
|
@ -367,7 +360,7 @@ const { handleSetContentCache, handleResetCache, handleClearCache } =
|
|||
<template #icon>
|
||||
<IconSettings class="h-full w-full" />
|
||||
</template>
|
||||
设置
|
||||
{{ $t("core.common.buttons.setting") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
type="secondary"
|
||||
|
@ -377,7 +370,7 @@ const { handleSetContentCache, handleResetCache, handleClearCache } =
|
|||
<template #icon>
|
||||
<IconSendPlaneFill class="h-full w-full" />
|
||||
</template>
|
||||
发布
|
||||
{{ $t("core.common.buttons.publish") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
|
|
|
@ -5,7 +5,6 @@ import {
|
|||
IconArrowRight,
|
||||
IconEye,
|
||||
IconEyeOff,
|
||||
IconTeam,
|
||||
IconAddCircle,
|
||||
IconRefreshLine,
|
||||
IconExternalLinkLine,
|
||||
|
@ -38,8 +37,10 @@ import FilterTag from "@/components/filter/FilterTag.vue";
|
|||
import FilterCleanButton from "@/components/filter/FilterCleanButton.vue";
|
||||
import { getNode } from "@formkit/core";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
const { t } = useI18n();
|
||||
|
||||
const settingModal = ref(false);
|
||||
const selectedSinglePage = ref<SinglePage>();
|
||||
|
@ -65,57 +66,52 @@ interface SortItem {
|
|||
|
||||
const VisibleItems: VisibleItem[] = [
|
||||
{
|
||||
label: "全部",
|
||||
label: t("core.page.filters.visible.items.all"),
|
||||
value: undefined,
|
||||
},
|
||||
{
|
||||
label: "公开",
|
||||
label: t("core.page.filters.visible.items.public"),
|
||||
value: "PUBLIC",
|
||||
},
|
||||
// TODO: 支持内部成员可访问
|
||||
// {
|
||||
// label: "内部成员可访问",
|
||||
// value: "INTERNAL",
|
||||
// },
|
||||
{
|
||||
label: "私有",
|
||||
label: t("core.page.filters.visible.items.private"),
|
||||
value: "PRIVATE",
|
||||
},
|
||||
];
|
||||
|
||||
const PublishStatusItems: PublishStatusItem[] = [
|
||||
{
|
||||
label: "全部",
|
||||
label: t("core.page.filters.status.items.all"),
|
||||
value: undefined,
|
||||
},
|
||||
{
|
||||
label: "已发布",
|
||||
label: t("core.page.filters.status.items.published"),
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
label: "未发布",
|
||||
label: t("core.page.filters.status.items.draft"),
|
||||
value: false,
|
||||
},
|
||||
];
|
||||
|
||||
const SortItems: SortItem[] = [
|
||||
{
|
||||
label: "较近发布",
|
||||
label: t("core.page.filters.sort.items.publish_time_desc"),
|
||||
sort: "PUBLISH_TIME",
|
||||
sortOrder: false,
|
||||
},
|
||||
{
|
||||
label: "较早发布",
|
||||
label: t("core.page.filters.sort.items.publish_time_asc"),
|
||||
sort: "PUBLISH_TIME",
|
||||
sortOrder: true,
|
||||
},
|
||||
{
|
||||
label: "较近创建",
|
||||
label: t("core.page.filters.sort.items.create_time_desc"),
|
||||
sort: "CREATE_TIME",
|
||||
sortOrder: false,
|
||||
},
|
||||
{
|
||||
label: "较早创建",
|
||||
label: t("core.page.filters.sort.items.create_time_asc"),
|
||||
sort: "CREATE_TIME",
|
||||
sortOrder: true,
|
||||
},
|
||||
|
@ -328,9 +324,11 @@ const handleCheckAllChange = (e: Event) => {
|
|||
|
||||
const handleDelete = async (singlePage: SinglePage) => {
|
||||
Dialog.warning({
|
||||
title: "确定要删除该自定义页面吗?",
|
||||
description: "该操作会将自定义页面放入回收站,后续可以从回收站恢复",
|
||||
title: t("core.page.operations.delete.title"),
|
||||
description: t("core.page.operations.delete.description"),
|
||||
confirmType: "danger",
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
const singlePageToUpdate = cloneDeep(singlePage);
|
||||
singlePageToUpdate.spec.deleted = true;
|
||||
|
@ -342,16 +340,18 @@ const handleDelete = async (singlePage: SinglePage) => {
|
|||
);
|
||||
await refetch();
|
||||
|
||||
Toast.success("删除成功");
|
||||
Toast.success(t("core.common.toast.delete_success"));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteInBatch = async () => {
|
||||
Dialog.warning({
|
||||
title: "确定要删除选中的自定义页面吗?",
|
||||
description: "该操作会将自定义页面放入回收站,后续可以从回收站恢复",
|
||||
title: t("core.page.operations.delete_in_batch.title"),
|
||||
description: t("core.page.operations.delete_in_batch.description"),
|
||||
confirmType: "danger",
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
await Promise.all(
|
||||
selectedPageNames.value.map((name) => {
|
||||
|
@ -380,14 +380,16 @@ const handleDeleteInBatch = async () => {
|
|||
await refetch();
|
||||
selectedPageNames.value = [];
|
||||
|
||||
Toast.success("删除成功");
|
||||
Toast.success(t("core.common.toast.delete_success"));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const getPublishStatus = (singlePage: SinglePage) => {
|
||||
const { labels } = singlePage.metadata;
|
||||
return labels?.[singlePageLabels.PUBLISHED] === "true" ? "已发布" : "未发布";
|
||||
return labels?.[singlePageLabels.PUBLISHED] === "true"
|
||||
? t("core.page.filters.status.items.published")
|
||||
: t("core.page.filters.status.items.draft");
|
||||
};
|
||||
|
||||
const isPublishing = (singlePage: SinglePage) => {
|
||||
|
@ -412,15 +414,15 @@ watch(selectedPageNames, (newValue) => {
|
|||
>
|
||||
<template #actions>
|
||||
<span @click="handleSelectPrevious">
|
||||
<IconArrowLeft v-tooltip="`上一项`" />
|
||||
<IconArrowLeft v-tooltip="$t('core.common.buttons.previous')" />
|
||||
</span>
|
||||
<span @click="handleSelectNext">
|
||||
<IconArrowRight v-tooltip="`下一项`" />
|
||||
<IconArrowRight v-tooltip="$t('core.common.buttons.next')" />
|
||||
</span>
|
||||
</template>
|
||||
</SinglePageSettingModal>
|
||||
|
||||
<VPageHeader title="页面">
|
||||
<VPageHeader :title="$t('core.page.title')">
|
||||
<template #icon>
|
||||
<IconPages class="mr-2 self-center" />
|
||||
</template>
|
||||
|
@ -431,7 +433,7 @@ watch(selectedPageNames, (newValue) => {
|
|||
:route="{ name: 'DeletedSinglePages' }"
|
||||
size="sm"
|
||||
>
|
||||
回收站
|
||||
{{ $t("core.page.actions.recycle_bin") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-permission="['system:singlepages:manage']"
|
||||
|
@ -441,7 +443,7 @@ watch(selectedPageNames, (newValue) => {
|
|||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
新建
|
||||
{{ $t("core.common.buttons.new") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
|
@ -473,7 +475,7 @@ watch(selectedPageNames, (newValue) => {
|
|||
<FormKit
|
||||
id="keywordInput"
|
||||
outer-class="!p-0"
|
||||
placeholder="输入关键词搜索"
|
||||
:placeholder="$t('core.common.placeholder.search')"
|
||||
type="text"
|
||||
name="keyword"
|
||||
:model-value="keyword"
|
||||
|
@ -481,35 +483,55 @@ watch(selectedPageNames, (newValue) => {
|
|||
></FormKit>
|
||||
|
||||
<FilterTag v-if="keyword" @close="handleClearKeyword()">
|
||||
关键词:{{ keyword }}
|
||||
{{
|
||||
$t("core.common.filters.results.keyword", {
|
||||
keyword: keyword,
|
||||
})
|
||||
}}
|
||||
</FilterTag>
|
||||
|
||||
<FilterTag
|
||||
v-if="selectedPublishStatusItem.value !== undefined"
|
||||
@close="handlePublishStatusItemChange(PublishStatusItems[0])"
|
||||
>
|
||||
状态:{{ selectedPublishStatusItem.label }}
|
||||
{{
|
||||
$t("core.common.filters.results.status", {
|
||||
status: selectedPublishStatusItem.label,
|
||||
})
|
||||
}}
|
||||
</FilterTag>
|
||||
|
||||
<FilterTag
|
||||
v-if="selectedVisibleItem.value"
|
||||
@close="handleVisibleItemChange(VisibleItems[0])"
|
||||
>
|
||||
可见性:{{ selectedVisibleItem.label }}
|
||||
{{
|
||||
$t("core.page.filters.visible.result", {
|
||||
visible: selectedVisibleItem.label,
|
||||
})
|
||||
}}
|
||||
</FilterTag>
|
||||
|
||||
<FilterTag
|
||||
v-if="selectedContributor"
|
||||
@close="handleSelectUser()"
|
||||
>
|
||||
作者:{{ selectedContributor?.spec.displayName }}
|
||||
{{
|
||||
$t("core.page.filters.author.result", {
|
||||
author: selectedContributor.spec.displayName,
|
||||
})
|
||||
}}
|
||||
</FilterTag>
|
||||
|
||||
<FilterTag
|
||||
v-if="selectedSortItem"
|
||||
@close="handleSortItemChange()"
|
||||
>
|
||||
排序:{{ selectedSortItem.label }}
|
||||
{{
|
||||
$t("core.common.filters.results.sort", {
|
||||
sort: selectedSortItem.label,
|
||||
})
|
||||
}}
|
||||
</FilterTag>
|
||||
|
||||
<FilterCleanButton
|
||||
|
@ -518,9 +540,9 @@ watch(selectedPageNames, (newValue) => {
|
|||
/>
|
||||
</div>
|
||||
<VSpace v-else>
|
||||
<VButton type="danger" @click="handleDeleteInBatch"
|
||||
>删除</VButton
|
||||
>
|
||||
<VButton type="danger" @click="handleDeleteInBatch">
|
||||
{{ $t("core.common.buttons.delete") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</div>
|
||||
<div class="mt-4 flex sm:mt-0">
|
||||
|
@ -529,7 +551,9 @@ watch(selectedPageNames, (newValue) => {
|
|||
<div
|
||||
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
||||
>
|
||||
<span class="mr-0.5">状态</span>
|
||||
<span class="mr-0.5">
|
||||
{{ $t("core.common.filters.labels.status") }}
|
||||
</span>
|
||||
<span>
|
||||
<IconArrowDown />
|
||||
</span>
|
||||
|
@ -559,7 +583,9 @@ watch(selectedPageNames, (newValue) => {
|
|||
<div
|
||||
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
||||
>
|
||||
<span class="mr-0.5"> 可见性 </span>
|
||||
<span class="mr-0.5">
|
||||
{{ $t("core.page.filters.visible.label") }}
|
||||
</span>
|
||||
<span>
|
||||
<IconArrowDown />
|
||||
</span>
|
||||
|
@ -593,7 +619,9 @@ watch(selectedPageNames, (newValue) => {
|
|||
<div
|
||||
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
||||
>
|
||||
<span class="mr-0.5">作者</span>
|
||||
<span class="mr-0.5">
|
||||
{{ $t("core.page.filters.author.label") }}
|
||||
</span>
|
||||
<span>
|
||||
<IconArrowDown />
|
||||
</span>
|
||||
|
@ -603,7 +631,9 @@ watch(selectedPageNames, (newValue) => {
|
|||
<div
|
||||
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
||||
>
|
||||
<span class="mr-0.5">排序</span>
|
||||
<span class="mr-0.5">
|
||||
{{ $t("core.common.filters.labels.sort") }}
|
||||
</span>
|
||||
<span>
|
||||
<IconArrowDown />
|
||||
</span>
|
||||
|
@ -630,7 +660,7 @@ watch(selectedPageNames, (newValue) => {
|
|||
@click="refetch()"
|
||||
>
|
||||
<IconRefreshLine
|
||||
v-tooltip="`刷新`"
|
||||
v-tooltip="$t('core.common.buttons.refresh')"
|
||||
:class="{ 'animate-spin text-gray-900': isFetching }"
|
||||
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
|
||||
/>
|
||||
|
@ -643,10 +673,15 @@ watch(selectedPageNames, (newValue) => {
|
|||
</template>
|
||||
<VLoading v-if="isLoading" />
|
||||
<Transition v-else-if="!singlePages?.length" appear name="fade">
|
||||
<VEmpty message="你可以尝试刷新或者新建页面" title="当前没有页面">
|
||||
<VEmpty
|
||||
:message="$t('core.page.empty.message')"
|
||||
:title="$t('core.page.empty.title')"
|
||||
>
|
||||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton @click="refetch">刷新</VButton>
|
||||
<VButton @click="refetch">
|
||||
{{ $t("core.common.buttons.refresh") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-permission="['system:singlepages:manage']"
|
||||
:route="{ name: 'SinglePageEditor' }"
|
||||
|
@ -655,7 +690,7 @@ watch(selectedPageNames, (newValue) => {
|
|||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
新建页面
|
||||
{{ $t("core.common.buttons.new") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
|
@ -691,7 +726,9 @@ watch(selectedPageNames, (newValue) => {
|
|||
<VSpace>
|
||||
<RouterLink
|
||||
v-if="singlePage.page.status?.inProgress"
|
||||
v-tooltip="`当前有内容已保存,但还未发布。`"
|
||||
v-tooltip="
|
||||
$t('core.common.tooltips.unpublished_content_tip')
|
||||
"
|
||||
:to="{
|
||||
name: 'SinglePageEditor',
|
||||
query: { name: singlePage.page.metadata.name },
|
||||
|
@ -715,10 +752,18 @@ watch(selectedPageNames, (newValue) => {
|
|||
<div class="flex w-full flex-col gap-1">
|
||||
<VSpace class="w-full">
|
||||
<span class="text-xs text-gray-500">
|
||||
访问量 {{ singlePage.stats.visit || 0 }}
|
||||
{{
|
||||
$t("core.page.list.fields.visits", {
|
||||
visits: singlePage.stats.visit || 0,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500">
|
||||
评论 {{ singlePage.stats.totalComment || 0 }}
|
||||
{{
|
||||
$t("core.page.list.fields.comments", {
|
||||
comments: singlePage.stats.totalComment || 0,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</VSpace>
|
||||
</div>
|
||||
|
@ -751,32 +796,33 @@ watch(selectedPageNames, (newValue) => {
|
|||
</VEntityField>
|
||||
<VEntityField :description="getPublishStatus(singlePage.page)">
|
||||
<template v-if="isPublishing(singlePage.page)" #description>
|
||||
<VStatusDot text="发布中" animate />
|
||||
<VStatusDot
|
||||
:text="$t('core.common.tooltips.publishing')"
|
||||
animate
|
||||
/>
|
||||
</template>
|
||||
</VEntityField>
|
||||
<VEntityField>
|
||||
<template #description>
|
||||
<IconEye
|
||||
v-if="singlePage.page.spec.visible === 'PUBLIC'"
|
||||
v-tooltip="`公开访问`"
|
||||
v-tooltip="$t('core.page.filters.visible.items.public')"
|
||||
class="cursor-pointer text-sm transition-all hover:text-blue-600"
|
||||
/>
|
||||
<IconEyeOff
|
||||
v-if="singlePage.page.spec.visible === 'PRIVATE'"
|
||||
v-tooltip="`私有访问`"
|
||||
class="cursor-pointer text-sm transition-all hover:text-blue-600"
|
||||
/>
|
||||
<!-- TODO: 支持内部成员可访问 -->
|
||||
<IconTeam
|
||||
v-if="false"
|
||||
v-tooltip="`内部成员可访问`"
|
||||
v-tooltip="$t('core.page.filters.visible.items.private')"
|
||||
class="cursor-pointer text-sm transition-all hover:text-blue-600"
|
||||
/>
|
||||
</template>
|
||||
</VEntityField>
|
||||
<VEntityField v-if="singlePage?.page?.spec.deleted">
|
||||
<template #description>
|
||||
<VStatusDot v-tooltip="`删除中`" state="warning" animate />
|
||||
<VStatusDot
|
||||
v-tooltip="$t('core.common.status.deleting')"
|
||||
state="warning"
|
||||
animate
|
||||
/>
|
||||
</template>
|
||||
</VEntityField>
|
||||
<VEntityField>
|
||||
|
@ -797,7 +843,7 @@ watch(selectedPageNames, (newValue) => {
|
|||
type="secondary"
|
||||
@click="handleOpenSettingModal(singlePage.page)"
|
||||
>
|
||||
设置
|
||||
{{ $t("core.common.buttons.setting") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-close-popper
|
||||
|
@ -805,7 +851,7 @@ watch(selectedPageNames, (newValue) => {
|
|||
type="danger"
|
||||
@click="handleDelete(singlePage.page)"
|
||||
>
|
||||
删除
|
||||
{{ $t("core.common.buttons.delete") }}
|
||||
</VButton>
|
||||
</template>
|
||||
</VEntity>
|
||||
|
@ -818,6 +864,8 @@ watch(selectedPageNames, (newValue) => {
|
|||
<VPagination
|
||||
v-model:page="page"
|
||||
v-model:size="size"
|
||||
:page-label="$t('core.components.pagination.page_label')"
|
||||
:size-label="$t('core.components.pagination.size_label')"
|
||||
:total="total"
|
||||
:size-options="[20, 30, 50, 100]"
|
||||
/>
|
||||
|
|
|
@ -18,6 +18,7 @@ import { submitForm } from "@formkit/core";
|
|||
import AnnotationsForm from "@/components/form/AnnotationsForm.vue";
|
||||
import useSlugify from "@/composables/use-slugify";
|
||||
import { useMutation } from "@tanstack/vue-query";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const initialFormState: SinglePage = {
|
||||
spec: {
|
||||
|
@ -67,6 +68,8 @@ const emit = defineEmits<{
|
|||
(event: "published", singlePage: SinglePage): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const formState = ref<SinglePage>(cloneDeep(initialFormState));
|
||||
const saving = ref(false);
|
||||
const publishing = ref(false);
|
||||
|
@ -142,7 +145,7 @@ const { mutateAsync: singlePageUpdateMutate } = useMutation({
|
|||
retry: 3,
|
||||
onError: (error) => {
|
||||
console.error("Failed to update post", error);
|
||||
Toast.error(`服务器内部错误`);
|
||||
Toast.error(t("core.common.toast.server_internal_error"));
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -182,7 +185,7 @@ const handleSave = async () => {
|
|||
|
||||
onVisibleChange(false);
|
||||
|
||||
Toast.success("保存成功");
|
||||
Toast.success(t("core.common.toast.save_success"));
|
||||
} catch (error) {
|
||||
console.error("Failed to save single page", error);
|
||||
} finally {
|
||||
|
@ -233,7 +236,7 @@ const handlePublish = async () => {
|
|||
|
||||
onVisibleChange(false);
|
||||
|
||||
Toast.success("发布成功");
|
||||
Toast.success(t("core.common.toast.publish_success"));
|
||||
} catch (error) {
|
||||
console.error("Failed to publish single page", error);
|
||||
} finally {
|
||||
|
@ -265,7 +268,7 @@ const handleUnpublish = async () => {
|
|||
|
||||
onVisibleChange(false);
|
||||
|
||||
Toast.success("取消发布成功");
|
||||
Toast.success(t("core.common.toast.cancel_publish_success"));
|
||||
} catch (error) {
|
||||
console.error("Failed to unpublish single page", error);
|
||||
} finally {
|
||||
|
@ -314,7 +317,7 @@ const { handleGenerateSlug } = useSlugify(
|
|||
<VModal
|
||||
:visible="visible"
|
||||
:width="700"
|
||||
title="页面设置"
|
||||
:title="$t('core.page.settings.title')"
|
||||
:centered="false"
|
||||
@update:visible="onVisibleChange"
|
||||
>
|
||||
|
@ -334,29 +337,31 @@ const { handleGenerateSlug } = useSlugify(
|
|||
<div class="md:col-span-1">
|
||||
<div class="sticky top-0">
|
||||
<span class="text-base font-medium text-gray-900">
|
||||
常规设置
|
||||
{{ $t("core.page.settings.groups.general") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0">
|
||||
<FormKit
|
||||
v-model="formState.spec.title"
|
||||
label="标题"
|
||||
:label="$t('core.page.settings.fields.title.label')"
|
||||
type="text"
|
||||
name="title"
|
||||
validation="required|length:0,100"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
v-model="formState.spec.slug"
|
||||
label="别名"
|
||||
:label="$t('core.page.settings.fields.slug.label')"
|
||||
name="slug"
|
||||
type="text"
|
||||
validation="required|length:0,100"
|
||||
help="通常用于生成页面的固定链接"
|
||||
:help="$t('core.page.settings.fields.slug.help')"
|
||||
>
|
||||
<template #suffix>
|
||||
<div
|
||||
v-tooltip="'根据标题重新生成别名'"
|
||||
v-tooltip="
|
||||
$t('core.page.settings.fields.slug.refresh_message')
|
||||
"
|
||||
class="group flex h-full cursor-pointer items-center border-l px-3 transition-all hover:bg-gray-100"
|
||||
@click="handleGenerateSlug"
|
||||
>
|
||||
|
@ -369,11 +374,13 @@ const { handleGenerateSlug } = useSlugify(
|
|||
<FormKit
|
||||
v-model="formState.spec.excerpt.autoGenerate"
|
||||
:options="[
|
||||
{ label: '是', value: true },
|
||||
{ label: '否', value: false },
|
||||
{ label: $t('core.common.radio.yes'), value: true },
|
||||
{ label: $t('core.common.radio.no'), value: false },
|
||||
]"
|
||||
name="autoGenerate"
|
||||
label="自动生成摘要"
|
||||
:label="
|
||||
$t('core.page.settings.fields.auto_generate_excerpt.label')
|
||||
"
|
||||
type="radio"
|
||||
>
|
||||
</FormKit>
|
||||
|
@ -381,7 +388,7 @@ const { handleGenerateSlug } = useSlugify(
|
|||
v-if="!formState.spec.excerpt.autoGenerate"
|
||||
v-model="formState.spec.excerpt.raw"
|
||||
name="raw"
|
||||
label="自定义摘要"
|
||||
:label="$t('core.page.settings.fields.raw_excerpt.label')"
|
||||
type="textarea"
|
||||
validation="length:0,1024"
|
||||
:rows="5"
|
||||
|
@ -397,7 +404,7 @@ const { handleGenerateSlug } = useSlugify(
|
|||
<div class="md:col-span-1">
|
||||
<div class="sticky top-0">
|
||||
<span class="text-base font-medium text-gray-900">
|
||||
高级设置
|
||||
{{ $t("core.page.settings.groups.advanced") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -405,36 +412,39 @@ const { handleGenerateSlug } = useSlugify(
|
|||
<FormKit
|
||||
v-model="formState.spec.allowComment"
|
||||
:options="[
|
||||
{ label: '是', value: true },
|
||||
{ label: '否', value: false },
|
||||
{ label: $t('core.common.radio.yes'), value: true },
|
||||
{ label: $t('core.common.radio.no'), value: false },
|
||||
]"
|
||||
name="allowComment"
|
||||
label="允许评论"
|
||||
:label="$t('core.page.settings.fields.allow_comment.label')"
|
||||
type="radio"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
v-model="formState.spec.pinned"
|
||||
:options="[
|
||||
{ label: '是', value: true },
|
||||
{ label: '否', value: false },
|
||||
{ label: $t('core.common.radio.yes'), value: true },
|
||||
{ label: $t('core.common.radio.no'), value: false },
|
||||
]"
|
||||
label="是否置顶"
|
||||
:label="$t('core.page.settings.fields.pinned.label')"
|
||||
name="pinned"
|
||||
type="radio"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
v-model="formState.spec.visible"
|
||||
:options="[
|
||||
{ label: '公开', value: 'PUBLIC' },
|
||||
{ label: '私有', value: 'PRIVATE' },
|
||||
{ label: $t('core.common.select.public'), value: 'PUBLIC' },
|
||||
{
|
||||
label: $t('core.common.select.private'),
|
||||
value: 'PRIVATE',
|
||||
},
|
||||
]"
|
||||
label="可见性"
|
||||
:label="$t('core.page.settings.fields.visible.label')"
|
||||
name="visible"
|
||||
type="select"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
:model-value="publishTime"
|
||||
label="发表时间"
|
||||
:label="$t('core.page.settings.fields.publish_time.label')"
|
||||
type="datetime-local"
|
||||
name="publishTime"
|
||||
@input="onPublishTimeChange"
|
||||
|
@ -442,13 +452,13 @@ const { handleGenerateSlug } = useSlugify(
|
|||
<FormKit
|
||||
v-model="formState.spec.template"
|
||||
:options="templates"
|
||||
label="自定义模板"
|
||||
:label="$t('core.page.settings.fields.template.label')"
|
||||
type="select"
|
||||
name="template"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
v-model="formState.spec.cover"
|
||||
label="封面图"
|
||||
:label="$t('core.page.settings.fields.cover.label')"
|
||||
type="attachment"
|
||||
name="cover"
|
||||
validation="length:0,1024"
|
||||
|
@ -465,7 +475,9 @@ const { handleGenerateSlug } = useSlugify(
|
|||
<div class="md:grid md:grid-cols-4 md:gap-6">
|
||||
<div class="md:col-span-1">
|
||||
<div class="sticky top-0">
|
||||
<span class="text-base font-medium text-gray-900"> 元数据 </span>
|
||||
<span class="text-base font-medium text-gray-900">
|
||||
{{ $t("core.page.settings.groups.annotations") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0">
|
||||
|
@ -490,7 +502,7 @@ const { handleGenerateSlug } = useSlugify(
|
|||
type="secondary"
|
||||
@click="handlePublishClick()"
|
||||
>
|
||||
发布
|
||||
{{ $t("core.common.buttons.publish") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-else
|
||||
|
@ -498,13 +510,15 @@ const { handleGenerateSlug } = useSlugify(
|
|||
type="danger"
|
||||
@click="handleUnpublish()"
|
||||
>
|
||||
取消发布
|
||||
{{ $t("core.common.buttons.cancel_publish") }}
|
||||
</VButton>
|
||||
</template>
|
||||
<VButton :loading="saving" type="secondary" @click="handleSaveClick">
|
||||
保存
|
||||
{{ $t("core.common.buttons.save") }}
|
||||
</VButton>
|
||||
<VButton type="default" @click="onVisibleChange(false)">
|
||||
{{ $t("core.common.buttons.close") }}
|
||||
</VButton>
|
||||
<VButton type="default" @click="onVisibleChange(false)"> 关闭 </VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
</VModal>
|
||||
|
|
|
@ -21,11 +21,11 @@ export default definePlugin({
|
|||
name: "SinglePages",
|
||||
component: SinglePageList,
|
||||
meta: {
|
||||
title: "页面",
|
||||
title: "core.page.title",
|
||||
searchable: true,
|
||||
permissions: ["system:singlepages:view"],
|
||||
menu: {
|
||||
name: "页面",
|
||||
name: "core.sidebar.menu.items.single_pages",
|
||||
group: "content",
|
||||
icon: markRaw(IconPages),
|
||||
priority: 1,
|
||||
|
@ -38,7 +38,7 @@ export default definePlugin({
|
|||
name: "DeletedSinglePages",
|
||||
component: DeletedSinglePageList,
|
||||
meta: {
|
||||
title: "页面回收站",
|
||||
title: "core.deleted_page.title",
|
||||
searchable: true,
|
||||
permissions: ["system:singlepages:view"],
|
||||
},
|
||||
|
@ -48,7 +48,7 @@ export default definePlugin({
|
|||
name: "SinglePageEditor",
|
||||
component: SinglePageEditor,
|
||||
meta: {
|
||||
title: "页面编辑",
|
||||
title: "core.page_editor.title",
|
||||
searchable: true,
|
||||
permissions: ["system:singlepages:manage"],
|
||||
},
|
||||
|
|
|
@ -31,7 +31,9 @@ onMounted(handleFetchSinglePages);
|
|||
</span>
|
||||
|
||||
<div>
|
||||
<span class="text-sm text-gray-500">页面</span>
|
||||
<span class="text-sm text-gray-500">
|
||||
{{ $t("core.dashboard.widgets.presets.page_stats.title") }}
|
||||
</span>
|
||||
<p class="text-2xl font-medium text-gray-900">
|
||||
{{ singlePageTotal || 0 }}
|
||||
</p>
|
||||
|
|
|
@ -27,8 +27,10 @@ import cloneDeep from "lodash.clonedeep";
|
|||
import { getNode } from "@formkit/core";
|
||||
import FilterTag from "@/components/filter/FilterTag.vue";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
const { t } = useI18n();
|
||||
|
||||
const checkedAll = ref(false);
|
||||
const selectedPostNames = ref<string[]>([]);
|
||||
|
@ -86,25 +88,29 @@ const handleCheckAllChange = (e: Event) => {
|
|||
|
||||
const handleDeletePermanently = async (post: Post) => {
|
||||
Dialog.warning({
|
||||
title: "确定要永久删除该文章吗?",
|
||||
description: "删除之后将无法恢复",
|
||||
title: t("core.deleted_post.operations.delete.title"),
|
||||
description: t("core.deleted_post.operations.delete.description"),
|
||||
confirmType: "danger",
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
await apiClient.extension.post.deletecontentHaloRunV1alpha1Post({
|
||||
name: post.metadata.name,
|
||||
});
|
||||
await refetch();
|
||||
|
||||
Toast.success("删除成功");
|
||||
Toast.success(t("core.common.toast.delete_success"));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeletePermanentlyInBatch = async () => {
|
||||
Dialog.warning({
|
||||
title: "确定要永久删除选中的文章吗?",
|
||||
description: "删除之后将无法恢复",
|
||||
title: t("core.deleted_post.operations.delete_in_batch.title"),
|
||||
description: t("core.deleted_post.operations.delete_in_batch.description"),
|
||||
confirmType: "danger",
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
await Promise.all(
|
||||
selectedPostNames.value.map((name) => {
|
||||
|
@ -116,15 +122,17 @@ const handleDeletePermanentlyInBatch = async () => {
|
|||
await refetch();
|
||||
selectedPostNames.value = [];
|
||||
|
||||
Toast.success("删除成功");
|
||||
Toast.success(t("core.common.toast.delete_success"));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleRecovery = async (post: Post) => {
|
||||
Dialog.warning({
|
||||
title: "确定要恢复该文章吗?",
|
||||
description: "该操作会将文章恢复到被删除之前的状态",
|
||||
title: t("core.deleted_post.operations.recovery.title"),
|
||||
description: t("core.deleted_post.operations.recovery.description"),
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
const postToUpdate = cloneDeep(post);
|
||||
postToUpdate.spec.deleted = false;
|
||||
|
@ -135,15 +143,19 @@ const handleRecovery = async (post: Post) => {
|
|||
|
||||
await refetch();
|
||||
|
||||
Toast.success("恢复成功");
|
||||
Toast.success(t("core.common.toast.recovery_success"));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleRecoveryInBatch = async () => {
|
||||
Dialog.warning({
|
||||
title: "确定要恢复选中的文章吗?",
|
||||
description: "该操作会将文章恢复到被删除之前的状态",
|
||||
title: t("core.deleted_post.operations.recovery_in_batch.title"),
|
||||
description: t(
|
||||
"core.deleted_post.operations.recovery_in_batch.description"
|
||||
),
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
await Promise.all(
|
||||
selectedPostNames.value.map((name) => {
|
||||
|
@ -170,7 +182,7 @@ const handleRecoveryInBatch = async () => {
|
|||
await refetch();
|
||||
selectedPostNames.value = [];
|
||||
|
||||
Toast.success("恢复成功");
|
||||
Toast.success(t("core.common.toast.recovery_success"));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -193,13 +205,15 @@ function handleClearKeyword() {
|
|||
}
|
||||
</script>
|
||||
<template>
|
||||
<VPageHeader title="文章回收站">
|
||||
<VPageHeader :title="$t('core.deleted_post.title')">
|
||||
<template #icon>
|
||||
<IconDeleteBin class="mr-2 self-center text-green-600" />
|
||||
</template>
|
||||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton :route="{ name: 'Posts' }" size="sm">返回</VButton>
|
||||
<VButton :route="{ name: 'Posts' }" size="sm">
|
||||
{{ $t("core.common.buttons.back") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-permission="['system:posts:manage']"
|
||||
:route="{ name: 'PostEditor' }"
|
||||
|
@ -208,7 +222,7 @@ function handleClearKeyword() {
|
|||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
新建
|
||||
{{ $t("core.common.buttons.new") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
|
@ -240,7 +254,7 @@ function handleClearKeyword() {
|
|||
<FormKit
|
||||
id="keywordInput"
|
||||
outer-class="!p-0"
|
||||
placeholder="输入关键词搜索"
|
||||
:placeholder="$t('core.common.placeholder.search')"
|
||||
type="text"
|
||||
name="keyword"
|
||||
:model-value="keyword"
|
||||
|
@ -248,15 +262,19 @@ function handleClearKeyword() {
|
|||
></FormKit>
|
||||
|
||||
<FilterTag v-if="keyword" @close="handleClearKeyword()">
|
||||
关键词:{{ keyword }}
|
||||
{{
|
||||
$t("core.common.filters.results.keyword", {
|
||||
keyword: keyword,
|
||||
})
|
||||
}}
|
||||
</FilterTag>
|
||||
</div>
|
||||
<VSpace v-else>
|
||||
<VButton type="danger" @click="handleDeletePermanentlyInBatch">
|
||||
永久删除
|
||||
{{ $t("core.common.buttons.delete_permanently") }}
|
||||
</VButton>
|
||||
<VButton type="default" @click="handleRecoveryInBatch">
|
||||
恢复
|
||||
{{ $t("core.common.buttons.recovery") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</div>
|
||||
|
@ -283,14 +301,16 @@ function handleClearKeyword() {
|
|||
|
||||
<Transition v-else-if="!posts?.length" appear name="fade">
|
||||
<VEmpty
|
||||
message="你可以尝试刷新或者返回文章管理"
|
||||
title="没有文章被放入回收站"
|
||||
:message="$t('core.deleted_post.empty.message')"
|
||||
:title="$t('core.deleted_post.empty.title')"
|
||||
>
|
||||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton @click="refetch">刷新</VButton>
|
||||
<VButton @click="refetch">
|
||||
{{ $t("core.common.buttons.refresh") }}
|
||||
</VButton>
|
||||
<VButton :route="{ name: 'Posts' }" type="primary">
|
||||
返回
|
||||
{{ $t("core.common.buttons.back") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
|
@ -325,7 +345,8 @@ function handleClearKeyword() {
|
|||
v-if="post.categories.length"
|
||||
class="inline-flex flex-wrap gap-1 text-xs text-gray-500"
|
||||
>
|
||||
分类:<span
|
||||
{{ $t("core.post.list.fields.categories") }}
|
||||
<span
|
||||
v-for="(category, categoryIndex) in post.categories"
|
||||
:key="categoryIndex"
|
||||
class="cursor-pointer hover:text-gray-900"
|
||||
|
@ -334,10 +355,18 @@ function handleClearKeyword() {
|
|||
</span>
|
||||
</p>
|
||||
<span class="text-xs text-gray-500">
|
||||
访问量 {{ post.stats.visit || 0 }}
|
||||
{{
|
||||
$t("core.post.list.fields.visits", {
|
||||
visits: post.stats.visit,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500">
|
||||
评论 {{ post.stats.totalComment || 0 }}
|
||||
{{
|
||||
$t("core.post.list.fields.comments", {
|
||||
comments: post.stats.totalComment || 0,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</VSpace>
|
||||
<VSpace v-if="post.tags.length" class="flex-wrap">
|
||||
|
@ -378,12 +407,20 @@ function handleClearKeyword() {
|
|||
</VEntityField>
|
||||
<VEntityField v-if="!post?.post?.spec.deleted">
|
||||
<template #description>
|
||||
<VStatusDot v-tooltip="`恢复中`" state="success" animate />
|
||||
<VStatusDot
|
||||
v-tooltip="$t('core.common.tooltips.recovering')"
|
||||
state="success"
|
||||
animate
|
||||
/>
|
||||
</template>
|
||||
</VEntityField>
|
||||
<VEntityField v-if="post?.post?.metadata.deletionTimestamp">
|
||||
<template #description>
|
||||
<VStatusDot v-tooltip="`删除中`" state="warning" animate />
|
||||
<VStatusDot
|
||||
v-tooltip="$t('core.common.status.deleting')"
|
||||
state="warning"
|
||||
animate
|
||||
/>
|
||||
</template>
|
||||
</VEntityField>
|
||||
<VEntityField>
|
||||
|
@ -404,7 +441,7 @@ function handleClearKeyword() {
|
|||
type="danger"
|
||||
@click="handleDeletePermanently(post.post)"
|
||||
>
|
||||
永久删除
|
||||
{{ $t("core.common.buttons.delete_permanently") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-close-popper
|
||||
|
@ -412,7 +449,7 @@ function handleClearKeyword() {
|
|||
type="default"
|
||||
@click="handleRecovery(post.post)"
|
||||
>
|
||||
恢复
|
||||
{{ $t("core.common.buttons.recovery") }}
|
||||
</VButton>
|
||||
</template>
|
||||
</VEntity>
|
||||
|
@ -425,6 +462,8 @@ function handleClearKeyword() {
|
|||
<VPagination
|
||||
v-model:page="page"
|
||||
v-model:size="size"
|
||||
:page-label="$t('core.components.pagination.page_label')"
|
||||
:size-label="$t('core.components.pagination.size_label')"
|
||||
:total="total"
|
||||
:size-options="[20, 30, 50, 100]"
|
||||
/>
|
||||
|
|
|
@ -11,7 +11,6 @@ import {
|
|||
Dialog,
|
||||
} from "@halo-dev/components";
|
||||
import PostSettingModal from "./components/PostSettingModal.vue";
|
||||
import PostPreviewModal from "./components/PostPreviewModal.vue";
|
||||
import type { Post, PostRequest } from "@halo-dev/api-client";
|
||||
import {
|
||||
computed,
|
||||
|
@ -34,8 +33,10 @@ import {
|
|||
} from "@/composables/use-editor-extension-points";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import EditorProviderSelector from "@/components/dropdown-selector/EditorProviderSelector.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
||||
// Editor providers
|
||||
const { editorProviders } = useEditorExtensionPoints();
|
||||
|
@ -90,7 +91,6 @@ const initialFormState: PostRequest = {
|
|||
|
||||
const formState = ref<PostRequest>(cloneDeep(initialFormState));
|
||||
const settingModal = ref(false);
|
||||
const previewModal = ref(false);
|
||||
const saving = ref(false);
|
||||
const publishing = ref(false);
|
||||
|
||||
|
@ -118,7 +118,7 @@ const handleSave = async () => {
|
|||
|
||||
// Set default title and slug
|
||||
if (!formState.value.post.spec.title) {
|
||||
formState.value.post.spec.title = "无标题文章";
|
||||
formState.value.post.spec.title = t("core.post_editor.untitled");
|
||||
}
|
||||
|
||||
if (!formState.value.post.spec.slug) {
|
||||
|
@ -140,12 +140,12 @@ const handleSave = async () => {
|
|||
name.value = data.metadata.name;
|
||||
}
|
||||
|
||||
Toast.success("保存成功");
|
||||
Toast.success(t("core.common.toast.save_success"));
|
||||
handleClearCache(name.value as string);
|
||||
await handleFetchContent();
|
||||
} catch (e) {
|
||||
console.error("Failed to save post", e);
|
||||
Toast.error("保存失败,请重试");
|
||||
Toast.error(t("core.common.toast.save_failed_and_retry"));
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
|
@ -187,11 +187,13 @@ const handlePublish = async () => {
|
|||
router.push({ name: "Posts" });
|
||||
}
|
||||
|
||||
Toast.success("发布成功", { duration: 2000 });
|
||||
Toast.success(t("core.common.toast.publish_success"), {
|
||||
duration: 2000,
|
||||
});
|
||||
handleClearCache(name.value as string);
|
||||
} catch (error) {
|
||||
console.error("Failed to publish post", error);
|
||||
Toast.error("发布失败,请重试");
|
||||
Toast.error(t("core.common.toast.publish_failed_and_retry"));
|
||||
} finally {
|
||||
publishing.value = false;
|
||||
}
|
||||
|
@ -249,8 +251,12 @@ const handleFetchContent = async () => {
|
|||
formState.value.post = data;
|
||||
} else {
|
||||
Dialog.warning({
|
||||
title: "警告",
|
||||
description: `未找到符合 ${data.rawType} 格式的编辑器,请检查是否已安装编辑器插件`,
|
||||
title: t("core.common.dialog.titles.warning"),
|
||||
description: t("core.common.dialog.descriptions.editor_not_found", {
|
||||
raw_type: data.rawType,
|
||||
}),
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: () => {
|
||||
router.back();
|
||||
},
|
||||
|
@ -341,8 +347,7 @@ const { handleSetContentCache, handleResetCache, handleClearCache } =
|
|||
@saved="onSettingSaved"
|
||||
@published="onSettingPublished"
|
||||
/>
|
||||
<PostPreviewModal v-model:visible="previewModal" :post="formState.post" />
|
||||
<VPageHeader title="文章">
|
||||
<VPageHeader :title="$t('core.post.title')">
|
||||
<template #icon>
|
||||
<IconBookRead class="mr-2 self-center" />
|
||||
</template>
|
||||
|
@ -353,21 +358,11 @@ const { handleSetContentCache, handleResetCache, handleClearCache } =
|
|||
:provider="currentEditorProvider"
|
||||
@select="handleChangeEditorProvider"
|
||||
/>
|
||||
|
||||
<!-- TODO: add preview post support -->
|
||||
<VButton
|
||||
v-if="false"
|
||||
size="sm"
|
||||
type="default"
|
||||
@click="previewModal = true"
|
||||
>
|
||||
预览
|
||||
</VButton>
|
||||
<VButton :loading="saving" size="sm" type="default" @click="handleSave">
|
||||
<template #icon>
|
||||
<IconSave class="h-full w-full" />
|
||||
</template>
|
||||
保存
|
||||
{{ $t("core.common.buttons.save") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-if="isUpdateMode"
|
||||
|
@ -378,7 +373,7 @@ const { handleSetContentCache, handleResetCache, handleClearCache } =
|
|||
<template #icon>
|
||||
<IconSettings class="h-full w-full" />
|
||||
</template>
|
||||
设置
|
||||
{{ $t("core.common.buttons.setting") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
type="secondary"
|
||||
|
@ -388,7 +383,7 @@ const { handleSetContentCache, handleResetCache, handleClearCache } =
|
|||
<template #icon>
|
||||
<IconSendPlaneFill class="h-full w-full" />
|
||||
</template>
|
||||
发布
|
||||
{{ $t("core.common.buttons.publish") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
|
|
|
@ -7,7 +7,6 @@ import {
|
|||
IconBookRead,
|
||||
IconEye,
|
||||
IconEyeOff,
|
||||
IconTeam,
|
||||
IconRefreshLine,
|
||||
IconExternalLinkLine,
|
||||
Dialog,
|
||||
|
@ -41,12 +40,14 @@ import { formatDatetime } from "@/utils/date";
|
|||
import { usePermission } from "@/utils/permission";
|
||||
import { postLabels } from "@/constants/labels";
|
||||
import FilterTag from "@/components/filter/FilterTag.vue";
|
||||
import FilteCleanButton from "@/components/filter/FilterCleanButton.vue";
|
||||
import FilterCleanButton from "@/components/filter/FilterCleanButton.vue";
|
||||
import { getNode } from "@formkit/core";
|
||||
import TagDropdownSelector from "@/components/dropdown-selector/TagDropdownSelector.vue";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
const { t } = useI18n();
|
||||
|
||||
const settingModal = ref(false);
|
||||
const selectedPost = ref<Post>();
|
||||
|
@ -59,7 +60,7 @@ interface VisibleItem {
|
|||
value?: "PUBLIC" | "INTERNAL" | "PRIVATE";
|
||||
}
|
||||
|
||||
interface PublishStatuItem {
|
||||
interface PublishStatusItem {
|
||||
label: string;
|
||||
value?: boolean;
|
||||
}
|
||||
|
@ -72,64 +73,59 @@ interface SortItem {
|
|||
|
||||
const VisibleItems: VisibleItem[] = [
|
||||
{
|
||||
label: "全部",
|
||||
label: t("core.post.filters.visible.items.all"),
|
||||
value: undefined,
|
||||
},
|
||||
{
|
||||
label: "公开",
|
||||
label: t("core.post.filters.visible.items.public"),
|
||||
value: "PUBLIC",
|
||||
},
|
||||
// TODO: 支持内部成员可访问
|
||||
// {
|
||||
// label: "内部成员可访问",
|
||||
// value: "INTERNAL",
|
||||
// },
|
||||
{
|
||||
label: "私有",
|
||||
label: t("core.post.filters.visible.items.private"),
|
||||
value: "PRIVATE",
|
||||
},
|
||||
];
|
||||
|
||||
const PublishStatuItems: PublishStatuItem[] = [
|
||||
const PublishStatusItems: PublishStatusItem[] = [
|
||||
{
|
||||
label: "全部",
|
||||
label: t("core.post.filters.status.items.all"),
|
||||
value: undefined,
|
||||
},
|
||||
{
|
||||
label: "已发布",
|
||||
label: t("core.post.filters.status.items.published"),
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
label: "未发布",
|
||||
label: t("core.post.filters.status.items.draft"),
|
||||
value: false,
|
||||
},
|
||||
];
|
||||
|
||||
const SortItems: SortItem[] = [
|
||||
{
|
||||
label: "较近发布",
|
||||
label: t("core.post.filters.sort.items.publish_time_desc"),
|
||||
sort: "PUBLISH_TIME",
|
||||
sortOrder: false,
|
||||
},
|
||||
{
|
||||
label: "较早发布",
|
||||
label: t("core.post.filters.sort.items.publish_time_asc"),
|
||||
sort: "PUBLISH_TIME",
|
||||
sortOrder: true,
|
||||
},
|
||||
{
|
||||
label: "较近创建",
|
||||
label: t("core.post.filters.sort.items.create_time_desc"),
|
||||
sort: "CREATE_TIME",
|
||||
sortOrder: false,
|
||||
},
|
||||
{
|
||||
label: "较早创建",
|
||||
label: t("core.post.filters.sort.items.create_time_asc"),
|
||||
sort: "CREATE_TIME",
|
||||
sortOrder: true,
|
||||
},
|
||||
];
|
||||
|
||||
const selectedVisibleItem = ref<VisibleItem>(VisibleItems[0]);
|
||||
const selectedPublishStatusItem = ref<PublishStatuItem>(PublishStatuItems[0]);
|
||||
const selectedPublishStatusItem = ref<PublishStatusItem>(PublishStatusItems[0]);
|
||||
const selectedSortItem = ref<SortItem>();
|
||||
const selectedCategory = ref<Category>();
|
||||
const selectedTag = ref<Tag>();
|
||||
|
@ -141,7 +137,7 @@ function handleVisibleItemChange(visibleItem: VisibleItem) {
|
|||
page.value = 1;
|
||||
}
|
||||
|
||||
function handlePublishStatusItemChange(publishStatusItem: PublishStatuItem) {
|
||||
function handlePublishStatusItemChange(publishStatusItem: PublishStatusItem) {
|
||||
selectedPublishStatusItem.value = publishStatusItem;
|
||||
page.value = 1;
|
||||
}
|
||||
|
@ -181,7 +177,7 @@ function handleClearKeyword() {
|
|||
|
||||
function handleClearFilters() {
|
||||
selectedVisibleItem.value = VisibleItems[0];
|
||||
selectedPublishStatusItem.value = PublishStatuItems[0];
|
||||
selectedPublishStatusItem.value = PublishStatusItems[0];
|
||||
selectedSortItem.value = undefined;
|
||||
selectedCategory.value = undefined;
|
||||
selectedTag.value = undefined;
|
||||
|
@ -351,7 +347,9 @@ const checkSelection = (post: Post) => {
|
|||
|
||||
const getPublishStatus = (post: Post) => {
|
||||
const { labels } = post.metadata;
|
||||
return labels?.[postLabels.PUBLISHED] === "true" ? "已发布" : "未发布";
|
||||
return labels?.[postLabels.PUBLISHED] === "true"
|
||||
? t("core.post.filters.status.items.published")
|
||||
: t("core.post.filters.status.items.draft");
|
||||
};
|
||||
|
||||
const isPublishing = (post: Post) => {
|
||||
|
@ -377,25 +375,29 @@ const handleCheckAllChange = (e: Event) => {
|
|||
|
||||
const handleDelete = async (post: Post) => {
|
||||
Dialog.warning({
|
||||
title: "确定要删除该文章吗?",
|
||||
description: "该操作会将文章放入回收站,后续可以从回收站恢复",
|
||||
title: t("core.post.operations.delete.title"),
|
||||
description: t("core.post.operations.delete.description"),
|
||||
confirmType: "danger",
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
await apiClient.post.recyclePost({
|
||||
name: post.metadata.name,
|
||||
});
|
||||
await refetch();
|
||||
|
||||
Toast.success("删除成功");
|
||||
Toast.success(t("core.common.toast.delete_success"));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteInBatch = async () => {
|
||||
Dialog.warning({
|
||||
title: "确定要删除选中的文章吗?",
|
||||
description: "该操作会将文章放入回收站,后续可以从回收站恢复",
|
||||
title: t("core.post.operations.delete_in_batch.title"),
|
||||
description: t("core.post.operations.delete_in_batch.description"),
|
||||
confirmType: "danger",
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
await Promise.all(
|
||||
selectedPostNames.value.map((name) => {
|
||||
|
@ -407,7 +409,7 @@ const handleDeleteInBatch = async () => {
|
|||
await refetch();
|
||||
selectedPostNames.value = [];
|
||||
|
||||
Toast.success("删除成功");
|
||||
Toast.success(t("core.common.toast.delete_success"));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -424,22 +426,28 @@ watch(selectedPostNames, (newValue) => {
|
|||
>
|
||||
<template #actions>
|
||||
<span @click="handleSelectPrevious">
|
||||
<IconArrowLeft v-tooltip="`上一项`" />
|
||||
<IconArrowLeft v-tooltip="$t('core.common.buttons.previous')" />
|
||||
</span>
|
||||
<span @click="handleSelectNext">
|
||||
<IconArrowRight v-tooltip="`下一项`" />
|
||||
<IconArrowRight v-tooltip="$t('core.common.buttons.next')" />
|
||||
</span>
|
||||
</template>
|
||||
</PostSettingModal>
|
||||
<VPageHeader title="文章">
|
||||
<VPageHeader :title="$t('core.post.title')">
|
||||
<template #icon>
|
||||
<IconBookRead class="mr-2 self-center" />
|
||||
</template>
|
||||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton :route="{ name: 'Categories' }" size="sm">分类</VButton>
|
||||
<VButton :route="{ name: 'Tags' }" size="sm">标签</VButton>
|
||||
<VButton :route="{ name: 'DeletedPosts' }" size="sm">回收站</VButton>
|
||||
<VButton :route="{ name: 'Categories' }" size="sm">
|
||||
{{ $t("core.post.actions.categories") }}
|
||||
</VButton>
|
||||
<VButton :route="{ name: 'Tags' }" size="sm">
|
||||
{{ $t("core.post.actions.tags") }}
|
||||
</VButton>
|
||||
<VButton :route="{ name: 'DeletedPosts' }" size="sm">
|
||||
{{ $t("core.post.actions.recycle_bin") }}
|
||||
</VButton>
|
||||
|
||||
<VButton
|
||||
v-permission="['system:posts:manage']"
|
||||
|
@ -449,7 +457,7 @@ watch(selectedPostNames, (newValue) => {
|
|||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
新建
|
||||
{{ $t("core.common.buttons.new") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
|
@ -481,7 +489,7 @@ watch(selectedPostNames, (newValue) => {
|
|||
<FormKit
|
||||
id="keywordInput"
|
||||
outer-class="!p-0"
|
||||
placeholder="输入关键词搜索"
|
||||
:placeholder="$t('core.common.placeholder.search')"
|
||||
type="text"
|
||||
name="keyword"
|
||||
:model-value="keyword"
|
||||
|
@ -489,56 +497,84 @@ watch(selectedPostNames, (newValue) => {
|
|||
></FormKit>
|
||||
|
||||
<FilterTag v-if="keyword" @close="handleClearKeyword()">
|
||||
关键词:{{ keyword }}
|
||||
{{
|
||||
$t("core.common.filters.results.keyword", {
|
||||
keyword: keyword,
|
||||
})
|
||||
}}
|
||||
</FilterTag>
|
||||
|
||||
<FilterTag
|
||||
v-if="selectedPublishStatusItem.value !== undefined"
|
||||
@close="handlePublishStatusItemChange(PublishStatuItems[0])"
|
||||
@close="handlePublishStatusItemChange(PublishStatusItems[0])"
|
||||
>
|
||||
状态:{{ selectedPublishStatusItem.label }}
|
||||
{{
|
||||
$t("core.common.filters.results.status", {
|
||||
status: selectedPublishStatusItem.label,
|
||||
})
|
||||
}}
|
||||
</FilterTag>
|
||||
|
||||
<FilterTag
|
||||
v-if="selectedVisibleItem.value"
|
||||
@close="handleVisibleItemChange(VisibleItems[0])"
|
||||
>
|
||||
可见性:{{ selectedVisibleItem.label }}
|
||||
{{
|
||||
$t("core.post.filters.visible.result", {
|
||||
visible: selectedVisibleItem.label,
|
||||
})
|
||||
}}
|
||||
</FilterTag>
|
||||
|
||||
<FilterTag
|
||||
v-if="selectedCategory"
|
||||
@close="handleCategoryChange()"
|
||||
>
|
||||
分类:{{ selectedCategory.spec.displayName }}
|
||||
{{
|
||||
$t("core.post.filters.category.result", {
|
||||
category: selectedCategory.spec.displayName,
|
||||
})
|
||||
}}
|
||||
</FilterTag>
|
||||
|
||||
<FilterTag v-if="selectedTag" @click="handleTagChange()">
|
||||
标签:{{ selectedTag.spec.displayName }}
|
||||
{{
|
||||
$t("core.post.filters.tag.result", {
|
||||
tag: selectedTag.spec.displayName,
|
||||
})
|
||||
}}
|
||||
</FilterTag>
|
||||
|
||||
<FilterTag
|
||||
v-if="selectedContributor"
|
||||
@close="handleContributorChange()"
|
||||
>
|
||||
作者:{{ selectedContributor.spec.displayName }}
|
||||
{{
|
||||
$t("core.post.filters.author.result", {
|
||||
author: selectedContributor.spec.displayName,
|
||||
})
|
||||
}}
|
||||
</FilterTag>
|
||||
|
||||
<FilterTag
|
||||
v-if="selectedSortItem"
|
||||
@close="handleSortItemChange()"
|
||||
>
|
||||
排序:{{ selectedSortItem.label }}
|
||||
{{
|
||||
$t("core.common.filters.results.sort", {
|
||||
sort: selectedSortItem.label,
|
||||
})
|
||||
}}
|
||||
</FilterTag>
|
||||
|
||||
<FilteCleanButton
|
||||
<FilterCleanButton
|
||||
v-if="hasFilters"
|
||||
@click="handleClearFilters"
|
||||
/>
|
||||
</div>
|
||||
<VSpace v-else>
|
||||
<VButton type="danger" @click="handleDeleteInBatch">
|
||||
删除
|
||||
{{ $t("core.common.buttons.delete") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</div>
|
||||
|
@ -548,7 +584,9 @@ watch(selectedPostNames, (newValue) => {
|
|||
<div
|
||||
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
||||
>
|
||||
<span class="mr-0.5">状态</span>
|
||||
<span class="mr-0.5">
|
||||
{{ $t("core.common.filters.labels.status") }}
|
||||
</span>
|
||||
<span>
|
||||
<IconArrowDown />
|
||||
</span>
|
||||
|
@ -557,7 +595,7 @@ watch(selectedPostNames, (newValue) => {
|
|||
<div class="w-72 p-4">
|
||||
<ul class="space-y-1">
|
||||
<li
|
||||
v-for="(filterItem, index) in PublishStatuItems"
|
||||
v-for="(filterItem, index) in PublishStatusItems"
|
||||
:key="index"
|
||||
v-close-popper
|
||||
:class="{
|
||||
|
@ -578,7 +616,9 @@ watch(selectedPostNames, (newValue) => {
|
|||
<div
|
||||
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
||||
>
|
||||
<span class="mr-0.5"> 可见性 </span>
|
||||
<span class="mr-0.5">
|
||||
{{ $t("core.post.filters.visible.label") }}
|
||||
</span>
|
||||
<span>
|
||||
<IconArrowDown />
|
||||
</span>
|
||||
|
@ -612,7 +652,9 @@ watch(selectedPostNames, (newValue) => {
|
|||
<div
|
||||
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
||||
>
|
||||
<span class="mr-0.5">分类</span>
|
||||
<span class="mr-0.5">
|
||||
{{ $t("core.post.filters.category.label") }}
|
||||
</span>
|
||||
<span>
|
||||
<IconArrowDown />
|
||||
</span>
|
||||
|
@ -625,7 +667,9 @@ watch(selectedPostNames, (newValue) => {
|
|||
<div
|
||||
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
||||
>
|
||||
<span class="mr-0.5">标签</span>
|
||||
<span class="mr-0.5">
|
||||
{{ $t("core.post.filters.tag.label") }}
|
||||
</span>
|
||||
<span>
|
||||
<IconArrowDown />
|
||||
</span>
|
||||
|
@ -638,7 +682,9 @@ watch(selectedPostNames, (newValue) => {
|
|||
<div
|
||||
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
||||
>
|
||||
<span class="mr-0.5">作者</span>
|
||||
<span class="mr-0.5">
|
||||
{{ $t("core.post.filters.author.label") }}
|
||||
</span>
|
||||
<span>
|
||||
<IconArrowDown />
|
||||
</span>
|
||||
|
@ -648,7 +694,9 @@ watch(selectedPostNames, (newValue) => {
|
|||
<div
|
||||
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
||||
>
|
||||
<span class="mr-0.5">排序</span>
|
||||
<span class="mr-0.5">
|
||||
{{ $t("core.common.filters.labels.sort") }}
|
||||
</span>
|
||||
<span>
|
||||
<IconArrowDown />
|
||||
</span>
|
||||
|
@ -675,7 +723,7 @@ watch(selectedPostNames, (newValue) => {
|
|||
@click="refetch()"
|
||||
>
|
||||
<IconRefreshLine
|
||||
v-tooltip="`刷新`"
|
||||
v-tooltip="$t('core.common.buttons.refresh')"
|
||||
:class="{ 'animate-spin text-gray-900': isFetching }"
|
||||
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
|
||||
/>
|
||||
|
@ -688,10 +736,15 @@ watch(selectedPostNames, (newValue) => {
|
|||
</template>
|
||||
<VLoading v-if="isLoading" />
|
||||
<Transition v-else-if="!posts?.length" appear name="fade">
|
||||
<VEmpty message="你可以尝试刷新或者新建文章" title="当前没有文章">
|
||||
<VEmpty
|
||||
:message="$t('core.post.empty.message')"
|
||||
:title="$t('core.post.empty.title')"
|
||||
>
|
||||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton @click="refetch">刷新</VButton>
|
||||
<VButton @click="refetch">
|
||||
{{ $t("core.common.buttons.refresh") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-permission="['system:posts:manage']"
|
||||
:route="{ name: 'PostEditor' }"
|
||||
|
@ -700,7 +753,7 @@ watch(selectedPostNames, (newValue) => {
|
|||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
新建文章
|
||||
{{ $t("core.common.buttons.new") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
|
@ -738,7 +791,9 @@ watch(selectedPostNames, (newValue) => {
|
|||
<VSpace class="mt-1 sm:mt-0">
|
||||
<RouterLink
|
||||
v-if="post.post.status?.inProgress"
|
||||
v-tooltip="`当前有内容已保存,但还未发布。`"
|
||||
v-tooltip="
|
||||
$t('core.common.tooltips.unpublished_content_tip')
|
||||
"
|
||||
:to="{
|
||||
name: 'PostEditor',
|
||||
query: { name: post.post.metadata.name },
|
||||
|
@ -765,7 +820,8 @@ watch(selectedPostNames, (newValue) => {
|
|||
v-if="post.categories.length"
|
||||
class="inline-flex flex-wrap gap-1 text-xs text-gray-500"
|
||||
>
|
||||
分类:<a
|
||||
{{ $t("core.post.list.fields.categories") }}
|
||||
<a
|
||||
v-for="(category, categoryIndex) in post.categories"
|
||||
:key="categoryIndex"
|
||||
:href="category.status?.permalink"
|
||||
|
@ -777,16 +833,24 @@ watch(selectedPostNames, (newValue) => {
|
|||
</a>
|
||||
</p>
|
||||
<span class="text-xs text-gray-500">
|
||||
访问量 {{ post.stats.visit || 0 }}
|
||||
{{
|
||||
$t("core.post.list.fields.visits", {
|
||||
visits: post.stats.visit,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500">
|
||||
评论 {{ post.stats.totalComment || 0 }}
|
||||
{{
|
||||
$t("core.post.list.fields.comments", {
|
||||
comments: post.stats.totalComment || 0,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span
|
||||
v-if="post.post.spec.pinned"
|
||||
class="text-xs text-gray-500"
|
||||
>
|
||||
已置顶
|
||||
{{ $t("core.post.list.fields.pinned") }}
|
||||
</span>
|
||||
</VSpace>
|
||||
<VSpace v-if="post.tags.length" class="flex-wrap">
|
||||
|
@ -827,32 +891,33 @@ watch(selectedPostNames, (newValue) => {
|
|||
</VEntityField>
|
||||
<VEntityField :description="getPublishStatus(post.post)">
|
||||
<template v-if="isPublishing(post.post)" #description>
|
||||
<VStatusDot text="发布中" animate />
|
||||
<VStatusDot
|
||||
:text="$t('core.common.tooltips.publishing')"
|
||||
animate
|
||||
/>
|
||||
</template>
|
||||
</VEntityField>
|
||||
<VEntityField>
|
||||
<template #description>
|
||||
<IconEye
|
||||
v-if="post.post.spec.visible === 'PUBLIC'"
|
||||
v-tooltip="`公开访问`"
|
||||
v-tooltip="$t('core.post.filters.visible.items.public')"
|
||||
class="cursor-pointer text-sm transition-all hover:text-blue-600"
|
||||
/>
|
||||
<IconEyeOff
|
||||
v-if="post.post.spec.visible === 'PRIVATE'"
|
||||
v-tooltip="`私有访问`"
|
||||
class="cursor-pointer text-sm transition-all hover:text-blue-600"
|
||||
/>
|
||||
<!-- TODO: 支持内部成员可访问 -->
|
||||
<IconTeam
|
||||
v-if="false"
|
||||
v-tooltip="`内部成员可访问`"
|
||||
v-tooltip="$t('core.post.filters.visible.items.private')"
|
||||
class="cursor-pointer text-sm transition-all hover:text-blue-600"
|
||||
/>
|
||||
</template>
|
||||
</VEntityField>
|
||||
<VEntityField v-if="post?.post?.spec.deleted">
|
||||
<template #description>
|
||||
<VStatusDot v-tooltip="`删除中`" state="warning" animate />
|
||||
<VStatusDot
|
||||
v-tooltip="$t('core.common.status.deleting')"
|
||||
state="warning"
|
||||
animate
|
||||
/>
|
||||
</template>
|
||||
</VEntityField>
|
||||
<VEntityField>
|
||||
|
@ -873,7 +938,7 @@ watch(selectedPostNames, (newValue) => {
|
|||
type="secondary"
|
||||
@click="handleOpenSettingModal(post.post)"
|
||||
>
|
||||
设置
|
||||
{{ $t("core.common.buttons.setting") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-close-popper
|
||||
|
@ -881,7 +946,7 @@ watch(selectedPostNames, (newValue) => {
|
|||
type="danger"
|
||||
@click="handleDelete(post.post)"
|
||||
>
|
||||
删除
|
||||
{{ $t("core.common.buttons.delete") }}
|
||||
</VButton>
|
||||
</template>
|
||||
</VEntity>
|
||||
|
@ -894,6 +959,8 @@ watch(selectedPostNames, (newValue) => {
|
|||
<VPagination
|
||||
v-model:page="page"
|
||||
v-model:size="size"
|
||||
:page-label="$t('core.components.pagination.page_label')"
|
||||
:size-label="$t('core.components.pagination.size_label')"
|
||||
:total="total"
|
||||
:size-options="[20, 30, 50, 100]"
|
||||
/>
|
||||
|
|
|
@ -79,7 +79,7 @@ const onEditingModalClose = () => {
|
|||
:category="selectedCategory"
|
||||
@close="onEditingModalClose"
|
||||
/>
|
||||
<VPageHeader title="文章分类">
|
||||
<VPageHeader :title="$t('core.post_category.title')">
|
||||
<template #icon>
|
||||
<IconBookRead class="mr-2 self-center" />
|
||||
</template>
|
||||
|
@ -93,7 +93,7 @@ const onEditingModalClose = () => {
|
|||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
新建
|
||||
{{ $t("core.common.buttons.new") }}
|
||||
</VButton>
|
||||
</template>
|
||||
</VPageHeader>
|
||||
|
@ -106,7 +106,11 @@ const onEditingModalClose = () => {
|
|||
>
|
||||
<div class="flex w-full flex-1 sm:w-auto">
|
||||
<span class="text-base font-medium">
|
||||
{{ categories?.length || 0 }} 个分类
|
||||
{{
|
||||
$t("core.post_category.header.title", {
|
||||
count: categories?.length || 0,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -114,10 +118,15 @@ const onEditingModalClose = () => {
|
|||
</template>
|
||||
<VLoading v-if="isLoading" />
|
||||
<Transition v-else-if="!categories?.length" appear name="fade">
|
||||
<VEmpty message="你可以尝试刷新或者新建分类" title="当前没有分类">
|
||||
<VEmpty
|
||||
:message="$t('core.post_category.empty.message')"
|
||||
:title="$t('core.post_category.empty.title')"
|
||||
>
|
||||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton @click="handleFetchCategories">刷新</VButton>
|
||||
<VButton @click="handleFetchCategories">
|
||||
{{ $t("core.common.buttons.refresh") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-permission="['system:posts:manage']"
|
||||
type="primary"
|
||||
|
@ -126,7 +135,7 @@ const onEditingModalClose = () => {
|
|||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
新建分类
|
||||
{{ $t("core.common.buttons.new") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
|
|
|
@ -23,6 +23,7 @@ import { setFocus } from "@/formkit/utils/focus";
|
|||
import { useThemeCustomTemplates } from "@/modules/interface/themes/composables/use-theme";
|
||||
import AnnotationsForm from "@/components/form/AnnotationsForm.vue";
|
||||
import useSlugify from "@/composables/use-slugify";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -40,6 +41,8 @@ const emit = defineEmits<{
|
|||
(event: "close"): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const initialFormState: Category = {
|
||||
spec: {
|
||||
displayName: "",
|
||||
|
@ -67,7 +70,9 @@ const isUpdateMode = computed(() => {
|
|||
});
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
return isUpdateMode.value ? "编辑文章分类" : "新增文章分类";
|
||||
return isUpdateMode.value
|
||||
? t("core.post_category.editing_modal.titles.update")
|
||||
: t("core.post_category.editing_modal.titles.create");
|
||||
});
|
||||
|
||||
const annotationsFormRef = ref<InstanceType<typeof AnnotationsForm>>();
|
||||
|
@ -101,7 +106,7 @@ const handleSaveCategory = async () => {
|
|||
}
|
||||
onVisibleChange(false);
|
||||
|
||||
Toast.success("保存成功");
|
||||
Toast.success(t("core.common.toast.save_success"));
|
||||
} catch (e) {
|
||||
console.error("Failed to create category", e);
|
||||
} finally {
|
||||
|
@ -178,7 +183,9 @@ const { handleGenerateSlug } = useSlugify(
|
|||
<div class="md:grid md:grid-cols-4 md:gap-6">
|
||||
<div class="md:col-span-1">
|
||||
<div class="sticky top-0">
|
||||
<span class="text-base font-medium text-gray-900"> 常规 </span>
|
||||
<span class="text-base font-medium text-gray-900">
|
||||
{{ $t("core.post_category.editing_modal.groups.general") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0">
|
||||
|
@ -186,21 +193,27 @@ const { handleGenerateSlug } = useSlugify(
|
|||
id="displayNameInput"
|
||||
v-model="formState.spec.displayName"
|
||||
name="displayName"
|
||||
label="名称"
|
||||
:label="
|
||||
$t('core.post_category.editing_modal.fields.display_name.label')
|
||||
"
|
||||
type="text"
|
||||
validation="required|length:0,50"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
v-model="formState.spec.slug"
|
||||
help="通常用于生成分类的固定链接"
|
||||
:help="$t('core.post_category.editing_modal.fields.slug.help')"
|
||||
name="slug"
|
||||
label="别名"
|
||||
:label="$t('core.post_category.editing_modal.fields.slug.label')"
|
||||
type="text"
|
||||
validation="required|length:0,50"
|
||||
>
|
||||
<template #suffix>
|
||||
<div
|
||||
v-tooltip="'根据名称重新生成别名'"
|
||||
v-tooltip="
|
||||
$t(
|
||||
'core.post_category.editing_modal.fields.slug.refresh_message'
|
||||
)
|
||||
"
|
||||
class="group flex h-full cursor-pointer items-center border-l px-3 transition-all hover:bg-gray-100"
|
||||
@click="handleGenerateSlug"
|
||||
>
|
||||
|
@ -213,23 +226,29 @@ const { handleGenerateSlug } = useSlugify(
|
|||
<FormKit
|
||||
v-model="formState.spec.template"
|
||||
:options="templates"
|
||||
label="自定义模板"
|
||||
:label="
|
||||
$t('core.post_category.editing_modal.fields.template.label')
|
||||
"
|
||||
type="select"
|
||||
name="template"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
v-model="formState.spec.cover"
|
||||
help="需要主题适配以支持"
|
||||
:help="$t('core.post_category.editing_modal.fields.cover.help')"
|
||||
name="cover"
|
||||
label="封面图"
|
||||
:label="$t('core.post_category.editing_modal.fields.cover.label')"
|
||||
type="attachment"
|
||||
validation="length:0,1024"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
v-model="formState.spec.description"
|
||||
name="description"
|
||||
help="需要主题适配以支持"
|
||||
label="描述"
|
||||
:help="
|
||||
$t('core.post_category.editing_modal.fields.description.help')
|
||||
"
|
||||
:label="
|
||||
$t('core.post_category.editing_modal.fields.description.label')
|
||||
"
|
||||
type="textarea"
|
||||
validation="length:0,200"
|
||||
></FormKit>
|
||||
|
@ -245,7 +264,9 @@ const { handleGenerateSlug } = useSlugify(
|
|||
<div class="md:grid md:grid-cols-4 md:gap-6">
|
||||
<div class="md:col-span-1">
|
||||
<div class="sticky top-0">
|
||||
<span class="text-base font-medium text-gray-900"> 元数据 </span>
|
||||
<span class="text-base font-medium text-gray-900">
|
||||
{{ $t("core.post_category.editing_modal.groups.annotations") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0">
|
||||
|
@ -265,10 +286,13 @@ const { handleGenerateSlug } = useSlugify(
|
|||
v-if="visible"
|
||||
:loading="saving"
|
||||
type="secondary"
|
||||
:text="$t('core.common.buttons.submit')"
|
||||
@submit="$formkit.submit('category-form')"
|
||||
>
|
||||
</SubmitButton>
|
||||
<VButton @click="onVisibleChange(false)">取消 Esc</VButton>
|
||||
<VButton @click="onVisibleChange(false)">
|
||||
{{ $t("core.common.buttons.cancel_and_shortcut") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
</VModal>
|
||||
|
|
|
@ -85,11 +85,19 @@ function onDelete(category: CategoryTree) {
|
|||
<template #end>
|
||||
<VEntityField v-if="category.metadata.deletionTimestamp">
|
||||
<template #description>
|
||||
<VStatusDot v-tooltip="`删除中`" state="warning" animate />
|
||||
<VStatusDot
|
||||
v-tooltip="$t('core.common.status.deleting')"
|
||||
state="warning"
|
||||
animate
|
||||
/>
|
||||
</template>
|
||||
</VEntityField>
|
||||
<VEntityField
|
||||
:description="`${category.status?.postCount || 0} 篇文章`"
|
||||
:description="
|
||||
$t('core.common.fields.post_count', {
|
||||
count: category.status?.postCount || 0,
|
||||
})
|
||||
"
|
||||
/>
|
||||
<VEntityField>
|
||||
<template #description>
|
||||
|
@ -110,7 +118,7 @@ function onDelete(category: CategoryTree) {
|
|||
type="secondary"
|
||||
@click="onOpenEditingModal(category)"
|
||||
>
|
||||
修改
|
||||
{{ $t("core.common.buttons.edit") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-permission="['system:posts:manage']"
|
||||
|
@ -119,7 +127,7 @@ function onDelete(category: CategoryTree) {
|
|||
type="danger"
|
||||
@click="onDelete(category)"
|
||||
>
|
||||
删除
|
||||
{{ $t("core.common.buttons.delete") }}
|
||||
</VButton>
|
||||
</template>
|
||||
</VEntity>
|
||||
|
|
|
@ -2,6 +2,8 @@ import { beforeEach, describe, expect, it } from "vitest";
|
|||
import { mount } from "@vue/test-utils";
|
||||
import CategoryEditingModal from "../CategoryEditingModal.vue";
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { createI18n } from "vue-i18n";
|
||||
import messages from "@intlify/unplugin-vue-i18n/messages";
|
||||
|
||||
describe("CategoryEditingModal", function () {
|
||||
beforeEach(() => {
|
||||
|
@ -9,6 +11,18 @@ describe("CategoryEditingModal", function () {
|
|||
});
|
||||
|
||||
it("should render", function () {
|
||||
expect(mount(CategoryEditingModal)).toBeDefined();
|
||||
expect(
|
||||
mount(CategoryEditingModal, {
|
||||
global: {
|
||||
plugins: [
|
||||
createI18n({
|
||||
legacy: false,
|
||||
locale: "en",
|
||||
messages,
|
||||
}),
|
||||
],
|
||||
},
|
||||
})
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@ import type { CategoryTree } from "@/modules/contents/posts/categories/utils";
|
|||
import { buildCategoriesTree } from "@/modules/contents/posts/categories/utils";
|
||||
import { Dialog, Toast } from "@halo-dev/components";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
interface usePostCategoryReturn {
|
||||
categories: Ref<Category[] | undefined>;
|
||||
|
@ -16,6 +17,8 @@ interface usePostCategoryReturn {
|
|||
}
|
||||
|
||||
export function usePostCategory(): usePostCategoryReturn {
|
||||
const { t } = useI18n();
|
||||
|
||||
const categoriesTree = ref<CategoryTree[]>([] as CategoryTree[]);
|
||||
|
||||
const {
|
||||
|
@ -48,9 +51,11 @@ export function usePostCategory(): usePostCategoryReturn {
|
|||
|
||||
const handleDelete = async (category: CategoryTree) => {
|
||||
Dialog.warning({
|
||||
title: "确定要删除该分类吗?",
|
||||
description: "删除此分类之后,对应文章的关联将被解除。该操作不可恢复。",
|
||||
title: t("core.post_category.operations.delete.title"),
|
||||
description: t("core.post_category.operations.delete.description"),
|
||||
confirmType: "danger",
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await apiClient.extension.category.deletecontentHaloRunV1alpha1Category(
|
||||
|
@ -59,7 +64,7 @@ export function usePostCategory(): usePostCategoryReturn {
|
|||
}
|
||||
);
|
||||
|
||||
Toast.success("删除成功");
|
||||
Toast.success(t("core.common.toast.delete_success"));
|
||||
} catch (e) {
|
||||
console.error("Failed to delete tag", e);
|
||||
} finally {
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
<script lang="ts" setup>
|
||||
import type { Post } from "@halo-dev/api-client";
|
||||
import { VModal, IconLink } from "@halo-dev/components";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
visible: boolean;
|
||||
post?: Post | null;
|
||||
}>(),
|
||||
{
|
||||
visible: false,
|
||||
post: null,
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "update:visible", visible: boolean): void;
|
||||
(event: "close"): void;
|
||||
}>();
|
||||
|
||||
const onVisibleChange = (visible: boolean) => {
|
||||
emit("update:visible", visible);
|
||||
if (!visible) {
|
||||
emit("close");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<VModal
|
||||
:body-class="['!p-0']"
|
||||
:visible="visible"
|
||||
:layer-closable="true"
|
||||
fullscreen
|
||||
title="文章预览"
|
||||
@update:visible="onVisibleChange"
|
||||
>
|
||||
<template #actions>
|
||||
<span>
|
||||
<a
|
||||
href="https://halo.run/archives/halo-154-released.html"
|
||||
target="_blank"
|
||||
>
|
||||
<IconLink />
|
||||
</a>
|
||||
</span>
|
||||
</template>
|
||||
<div class="flex h-full items-center justify-center">
|
||||
<iframe
|
||||
v-if="visible"
|
||||
class="h-full w-full border-none transition-all duration-300"
|
||||
src="https://halo.run/archives/halo-154-released.html"
|
||||
></iframe>
|
||||
</div>
|
||||
</VModal>
|
||||
</template>
|
|
@ -18,6 +18,7 @@ import AnnotationsForm from "@/components/form/AnnotationsForm.vue";
|
|||
import { submitForm } from "@formkit/core";
|
||||
import useSlugify from "@/composables/use-slugify";
|
||||
import { useMutation } from "@tanstack/vue-query";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const initialFormState: Post = {
|
||||
spec: {
|
||||
|
@ -69,6 +70,8 @@ const emit = defineEmits<{
|
|||
(event: "published", post: Post): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const formState = ref<Post>(cloneDeep(initialFormState));
|
||||
const saving = ref(false);
|
||||
const publishing = ref(false);
|
||||
|
@ -146,7 +149,7 @@ const { mutateAsync: postUpdateMutate } = useMutation({
|
|||
retry: 3,
|
||||
onError: (error) => {
|
||||
console.error("Failed to update post", error);
|
||||
Toast.error(`服务器内部错误`);
|
||||
Toast.error(t("core.common.toast.server_internal_error"));
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -185,7 +188,7 @@ const handleSave = async () => {
|
|||
|
||||
handleVisibleChange(false);
|
||||
|
||||
Toast.success("保存成功");
|
||||
Toast.success(t("core.common.toast.save_success"));
|
||||
} catch (e) {
|
||||
console.error("Failed to save post", e);
|
||||
} finally {
|
||||
|
@ -212,7 +215,7 @@ const handlePublish = async () => {
|
|||
|
||||
handleVisibleChange(false);
|
||||
|
||||
Toast.success("发布成功");
|
||||
Toast.success(t("core.common.toast.publish_success"));
|
||||
} catch (e) {
|
||||
console.error("Failed to publish post", e);
|
||||
} finally {
|
||||
|
@ -230,7 +233,7 @@ const handleUnpublish = async () => {
|
|||
|
||||
handleVisibleChange(false);
|
||||
|
||||
Toast.success("取消发布成功");
|
||||
Toast.success(t("core.common.toast.cancel_publish_success"));
|
||||
} catch (e) {
|
||||
console.error("Failed to publish post", e);
|
||||
} finally {
|
||||
|
@ -280,7 +283,7 @@ const { handleGenerateSlug } = useSlugify(
|
|||
<VModal
|
||||
:visible="visible"
|
||||
:width="700"
|
||||
title="文章设置"
|
||||
:title="$t('core.post.settings.title')"
|
||||
:centered="false"
|
||||
@update:visible="handleVisibleChange"
|
||||
>
|
||||
|
@ -300,29 +303,31 @@ const { handleGenerateSlug } = useSlugify(
|
|||
<div class="md:col-span-1">
|
||||
<div class="sticky top-0">
|
||||
<span class="text-base font-medium text-gray-900">
|
||||
常规设置
|
||||
{{ $t("core.post.settings.groups.general") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0">
|
||||
<FormKit
|
||||
v-model="formState.spec.title"
|
||||
label="标题"
|
||||
:label="$t('core.post.settings.fields.title.label')"
|
||||
type="text"
|
||||
name="title"
|
||||
validation="required|length:0,100"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
v-model="formState.spec.slug"
|
||||
label="别名"
|
||||
:label="$t('core.post.settings.fields.slug.label')"
|
||||
name="slug"
|
||||
type="text"
|
||||
validation="required|length:0,100"
|
||||
help="通常用于生成文章的固定链接"
|
||||
:help="$t('core.post.settings.fields.slug.help')"
|
||||
>
|
||||
<template #suffix>
|
||||
<div
|
||||
v-tooltip="'根据标题重新生成别名'"
|
||||
v-tooltip="
|
||||
$t('core.post.settings.fields.slug.refresh_message')
|
||||
"
|
||||
class="group flex h-full cursor-pointer items-center border-l px-3 transition-all hover:bg-gray-100"
|
||||
@click="handleGenerateSlug"
|
||||
>
|
||||
|
@ -334,14 +339,14 @@ const { handleGenerateSlug } = useSlugify(
|
|||
</FormKit>
|
||||
<FormKit
|
||||
v-model="formState.spec.categories"
|
||||
label="分类目录"
|
||||
:label="$t('core.post.settings.fields.categories.label')"
|
||||
name="categories"
|
||||
type="categorySelect"
|
||||
:multiple="true"
|
||||
/>
|
||||
<FormKit
|
||||
v-model="formState.spec.tags"
|
||||
label="标签"
|
||||
:label="$t('core.post.settings.fields.tags.label')"
|
||||
name="tags"
|
||||
type="tagSelect"
|
||||
:multiple="true"
|
||||
|
@ -349,18 +354,20 @@ const { handleGenerateSlug } = useSlugify(
|
|||
<FormKit
|
||||
v-model="formState.spec.excerpt.autoGenerate"
|
||||
:options="[
|
||||
{ label: '是', value: true },
|
||||
{ label: '否', value: false },
|
||||
{ label: $t('core.common.radio.yes'), value: true },
|
||||
{ label: $t('core.common.radio.no'), value: false },
|
||||
]"
|
||||
name="autoGenerate"
|
||||
label="自动生成摘要"
|
||||
:label="
|
||||
$t('core.post.settings.fields.auto_generate_excerpt.label')
|
||||
"
|
||||
type="radio"
|
||||
>
|
||||
</FormKit>
|
||||
<FormKit
|
||||
v-if="!formState.spec.excerpt.autoGenerate"
|
||||
v-model="formState.spec.excerpt.raw"
|
||||
label="自定义摘要"
|
||||
:label="$t('core.post.settings.fields.raw_excerpt.label')"
|
||||
name="raw"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
|
@ -377,7 +384,7 @@ const { handleGenerateSlug } = useSlugify(
|
|||
<div class="md:col-span-1">
|
||||
<div class="sticky top-0">
|
||||
<span class="text-base font-medium text-gray-900">
|
||||
高级设置
|
||||
{{ $t("core.post.settings.groups.advanced") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -385,49 +392,52 @@ const { handleGenerateSlug } = useSlugify(
|
|||
<FormKit
|
||||
v-model="formState.spec.allowComment"
|
||||
:options="[
|
||||
{ label: '是', value: true },
|
||||
{ label: '否', value: false },
|
||||
{ label: $t('core.common.radio.yes'), value: true },
|
||||
{ label: $t('core.common.radio.no'), value: false },
|
||||
]"
|
||||
label="允许评论"
|
||||
:label="$t('core.post.settings.fields.allow_comment.label')"
|
||||
type="radio"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
v-model="formState.spec.pinned"
|
||||
:options="[
|
||||
{ label: '是', value: true },
|
||||
{ label: '否', value: false },
|
||||
{ label: $t('core.common.radio.yes'), value: true },
|
||||
{ label: $t('core.common.radio.no'), value: false },
|
||||
]"
|
||||
label="是否置顶"
|
||||
:label="$t('core.post.settings.fields.pinned.label')"
|
||||
name="pinned"
|
||||
type="radio"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
v-model="formState.spec.visible"
|
||||
:options="[
|
||||
{ label: '公开', value: 'PUBLIC' },
|
||||
{ label: '私有', value: 'PRIVATE' },
|
||||
{ label: $t('core.common.select.public'), value: 'PUBLIC' },
|
||||
{
|
||||
label: $t('core.common.select.private'),
|
||||
value: 'PRIVATE',
|
||||
},
|
||||
]"
|
||||
label="可见性"
|
||||
:label="$t('core.post.settings.fields.visible.label')"
|
||||
name="visible"
|
||||
type="select"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
:model-value="publishTime"
|
||||
label="发表时间"
|
||||
:label="$t('core.post.settings.fields.publish_time.label')"
|
||||
type="datetime-local"
|
||||
@input="onPublishTimeChange"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
v-model="formState.spec.template"
|
||||
:options="templates"
|
||||
label="自定义模板"
|
||||
:label="$t('core.post.settings.fields.template.label')"
|
||||
name="template"
|
||||
type="select"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
v-model="formState.spec.cover"
|
||||
name="cover"
|
||||
label="封面图"
|
||||
:label="$t('core.post.settings.fields.cover.label')"
|
||||
type="attachment"
|
||||
validation="length:0,1024"
|
||||
></FormKit>
|
||||
|
@ -443,7 +453,9 @@ const { handleGenerateSlug } = useSlugify(
|
|||
<div class="md:grid md:grid-cols-4 md:gap-6">
|
||||
<div class="md:col-span-1">
|
||||
<div class="sticky top-0">
|
||||
<span class="text-base font-medium text-gray-900"> 元数据 </span>
|
||||
<span class="text-base font-medium text-gray-900">
|
||||
{{ $t("core.post.settings.groups.annotations") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0">
|
||||
|
@ -466,7 +478,7 @@ const { handleGenerateSlug } = useSlugify(
|
|||
type="secondary"
|
||||
@click="handlePublishClick()"
|
||||
>
|
||||
发布
|
||||
{{ $t("core.common.buttons.publish") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-else
|
||||
|
@ -474,14 +486,14 @@ const { handleGenerateSlug } = useSlugify(
|
|||
type="danger"
|
||||
@click="handleUnpublish()"
|
||||
>
|
||||
取消发布
|
||||
{{ $t("core.common.buttons.cancel_publish") }}
|
||||
</VButton>
|
||||
</template>
|
||||
<VButton :loading="saving" type="secondary" @click="handleSaveClick()">
|
||||
保存
|
||||
{{ $t("core.common.buttons.save") }}
|
||||
</VButton>
|
||||
<VButton type="default" @click="handleVisibleChange(false)">
|
||||
关闭
|
||||
{{ $t("core.common.buttons.close") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
|
|
|
@ -3,6 +3,8 @@ import { mount } from "@vue/test-utils";
|
|||
import PostSettingModal from "../PostSettingModal.vue";
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { VueQueryPlugin } from "@tanstack/vue-query";
|
||||
import { createI18n } from "vue-i18n";
|
||||
import messages from "@intlify/unplugin-vue-i18n/messages";
|
||||
|
||||
describe("PostSettingModal", () => {
|
||||
beforeEach(() => {
|
||||
|
@ -19,7 +21,14 @@ describe("PostSettingModal", () => {
|
|||
},
|
||||
{
|
||||
global: {
|
||||
plugins: [VueQueryPlugin],
|
||||
plugins: [
|
||||
VueQueryPlugin,
|
||||
createI18n({
|
||||
legacy: false,
|
||||
locale: "en",
|
||||
messages,
|
||||
}),
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
|
@ -26,11 +26,11 @@ export default definePlugin({
|
|||
name: "Posts",
|
||||
component: PostList,
|
||||
meta: {
|
||||
title: "文章",
|
||||
title: "core.post.title",
|
||||
searchable: true,
|
||||
permissions: ["system:posts:view"],
|
||||
menu: {
|
||||
name: "文章",
|
||||
name: "core.sidebar.menu.items.posts",
|
||||
group: "content",
|
||||
icon: markRaw(IconBookRead),
|
||||
priority: 0,
|
||||
|
@ -43,7 +43,7 @@ export default definePlugin({
|
|||
name: "DeletedPosts",
|
||||
component: DeletedPostList,
|
||||
meta: {
|
||||
title: "文章回收站",
|
||||
title: "core.deleted_post.title",
|
||||
searchable: true,
|
||||
permissions: ["system:posts:view"],
|
||||
},
|
||||
|
@ -53,7 +53,7 @@ export default definePlugin({
|
|||
name: "PostEditor",
|
||||
component: PostEditor,
|
||||
meta: {
|
||||
title: "文章编辑",
|
||||
title: "core.post_editor.title",
|
||||
searchable: true,
|
||||
permissions: ["system:posts:manage"],
|
||||
},
|
||||
|
@ -67,7 +67,7 @@ export default definePlugin({
|
|||
name: "Categories",
|
||||
component: CategoryList,
|
||||
meta: {
|
||||
title: "文章分类",
|
||||
title: "core.post_category.title",
|
||||
searchable: true,
|
||||
permissions: ["system:posts:view"],
|
||||
},
|
||||
|
@ -83,7 +83,7 @@ export default definePlugin({
|
|||
name: "Tags",
|
||||
component: TagList,
|
||||
meta: {
|
||||
title: "文章标签",
|
||||
title: "core.post_tag.title",
|
||||
searchable: true,
|
||||
permissions: ["system:posts:view"],
|
||||
},
|
||||
|
|
|
@ -116,7 +116,7 @@ onMounted(async () => {
|
|||
@next="handleSelectNext"
|
||||
@previous="handleSelectPrevious"
|
||||
/>
|
||||
<VPageHeader title="文章标签">
|
||||
<VPageHeader :title="$t('core.post_tag.title')">
|
||||
<template #icon>
|
||||
<IconBookRead class="mr-2 self-center" />
|
||||
</template>
|
||||
|
@ -129,7 +129,7 @@ onMounted(async () => {
|
|||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
新建
|
||||
{{ $t("core.common.buttons.new") }}
|
||||
</VButton>
|
||||
</template>
|
||||
</VPageHeader>
|
||||
|
@ -142,7 +142,9 @@ onMounted(async () => {
|
|||
>
|
||||
<div class="flex w-full flex-1 sm:w-auto">
|
||||
<span class="text-base font-medium">
|
||||
{{ tags?.length || 0 }} 个标签
|
||||
{{
|
||||
$t("core.post_tag.header.title", { count: tags?.length || 0 })
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2">
|
||||
|
@ -163,15 +165,20 @@ onMounted(async () => {
|
|||
</template>
|
||||
<VLoading v-if="isLoading" />
|
||||
<Transition v-else-if="!tags?.length" appear name="fade">
|
||||
<VEmpty message="你可以尝试刷新或者新建标签" title="当前没有标签">
|
||||
<VEmpty
|
||||
:message="$t('core.post_tag.empty.message')"
|
||||
:title="$t('core.post_tag.empty.title')"
|
||||
>
|
||||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton @click="handleFetchTags">刷新</VButton>
|
||||
<VButton @click="handleFetchTags">
|
||||
{{ $t("core.common.buttons.refresh") }}
|
||||
</VButton>
|
||||
<VButton type="primary" @click="editingModal = true">
|
||||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
新建标签
|
||||
{{ $t("core.common.buttons.new") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
|
@ -210,14 +217,18 @@ onMounted(async () => {
|
|||
<VEntityField v-if="tag.metadata.deletionTimestamp">
|
||||
<template #description>
|
||||
<VStatusDot
|
||||
v-tooltip="`删除中`"
|
||||
v-tooltip="$t('core.common.status.deleting')"
|
||||
state="warning"
|
||||
animate
|
||||
/>
|
||||
</template>
|
||||
</VEntityField>
|
||||
<VEntityField
|
||||
:description="`${tag.status?.postCount || 0} 篇文章`"
|
||||
:description="
|
||||
$t('core.common.fields.post_count', {
|
||||
count: tag.status?.postCount || 0,
|
||||
})
|
||||
"
|
||||
/>
|
||||
<VEntityField>
|
||||
<template #description>
|
||||
|
@ -238,7 +249,7 @@ onMounted(async () => {
|
|||
type="secondary"
|
||||
@click="handleOpenEditingModal(tag)"
|
||||
>
|
||||
修改
|
||||
{{ $t("core.common.buttons.edit") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-permission="['system:posts:manage']"
|
||||
|
@ -247,7 +258,7 @@ onMounted(async () => {
|
|||
type="danger"
|
||||
@click="handleDelete(tag)"
|
||||
>
|
||||
删除
|
||||
{{ $t("core.common.buttons.delete") }}
|
||||
</VButton>
|
||||
</template>
|
||||
</VEntity>
|
||||
|
|
|
@ -24,6 +24,7 @@ import { reset } from "@formkit/core";
|
|||
import { setFocus } from "@/formkit/utils/focus";
|
||||
import AnnotationsForm from "@/components/form/AnnotationsForm.vue";
|
||||
import useSlugify from "@/composables/use-slugify";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -43,6 +44,8 @@ const emit = defineEmits<{
|
|||
(event: "next"): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const initialFormState: Tag = {
|
||||
spec: {
|
||||
displayName: "",
|
||||
|
@ -66,7 +69,9 @@ const isUpdateMode = computed(() => {
|
|||
});
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
return isUpdateMode.value ? "编辑文章标签" : "新增文章标签";
|
||||
return isUpdateMode.value
|
||||
? t("core.post_tag.editing_modal.titles.update")
|
||||
: t("core.post_tag.editing_modal.titles.create");
|
||||
});
|
||||
|
||||
const annotationsFormRef = ref<InstanceType<typeof AnnotationsForm>>();
|
||||
|
@ -100,7 +105,7 @@ const handleSaveTag = async () => {
|
|||
}
|
||||
onVisibleChange(false);
|
||||
|
||||
Toast.success("保存成功");
|
||||
Toast.success(t("core.common.toast.save_success"));
|
||||
} catch (e) {
|
||||
console.error("Failed to create tag", e);
|
||||
} finally {
|
||||
|
@ -183,7 +188,9 @@ const { handleGenerateSlug } = useSlugify(
|
|||
<div class="md:grid md:grid-cols-4 md:gap-6">
|
||||
<div class="md:col-span-1">
|
||||
<div class="sticky top-0">
|
||||
<span class="text-base font-medium text-gray-900"> 常规 </span>
|
||||
<span class="text-base font-medium text-gray-900">
|
||||
{{ $t("core.post_tag.editing_modal.groups.general") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0">
|
||||
|
@ -191,21 +198,27 @@ const { handleGenerateSlug } = useSlugify(
|
|||
id="displayNameInput"
|
||||
v-model="formState.spec.displayName"
|
||||
name="displayName"
|
||||
label="名称"
|
||||
:label="
|
||||
$t('core.post_tag.editing_modal.fields.display_name.label')
|
||||
"
|
||||
type="text"
|
||||
validation="required|length:0,50"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
v-model="formState.spec.slug"
|
||||
help="通常用于生成标签的固定链接"
|
||||
label="别名"
|
||||
:help="$t('core.post_tag.editing_modal.fields.slug.help')"
|
||||
:label="$t('core.post_tag.editing_modal.fields.slug.label')"
|
||||
name="slug"
|
||||
type="text"
|
||||
validation="required|length:0,50"
|
||||
>
|
||||
<template #suffix>
|
||||
<div
|
||||
v-tooltip="'根据名称重新生成别名'"
|
||||
v-tooltip="
|
||||
$t(
|
||||
'core.post_tag.editing_modal.fields.slug.refresh_message'
|
||||
)
|
||||
"
|
||||
class="group flex h-full cursor-pointer items-center border-l px-3 transition-all hover:bg-gray-100"
|
||||
@click="handleGenerateSlug"
|
||||
>
|
||||
|
@ -218,16 +231,16 @@ const { handleGenerateSlug } = useSlugify(
|
|||
<FormKit
|
||||
v-model="formState.spec.color"
|
||||
name="color"
|
||||
help="需要主题适配以支持"
|
||||
label="颜色"
|
||||
:help="$t('core.post_tag.editing_modal.fields.color.help')"
|
||||
:label="$t('core.post_tag.editing_modal.fields.color.label')"
|
||||
type="color"
|
||||
validation="length:0,50"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
v-model="formState.spec.cover"
|
||||
name="cover"
|
||||
help="需要主题适配以支持"
|
||||
label="封面图"
|
||||
:help="$t('core.post_tag.editing_modal.fields.cover.help')"
|
||||
:label="$t('core.post_tag.editing_modal.fields.cover.label')"
|
||||
type="attachment"
|
||||
validation="length:0,1024"
|
||||
></FormKit>
|
||||
|
@ -243,7 +256,9 @@ const { handleGenerateSlug } = useSlugify(
|
|||
<div class="md:grid md:grid-cols-4 md:gap-6">
|
||||
<div class="md:col-span-1">
|
||||
<div class="sticky top-0">
|
||||
<span class="text-base font-medium text-gray-900"> 元数据 </span>
|
||||
<span class="text-base font-medium text-gray-900">
|
||||
{{ $t("core.post_tag.editing_modal.groups.annotations") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0">
|
||||
|
@ -263,10 +278,13 @@ const { handleGenerateSlug } = useSlugify(
|
|||
v-if="visible"
|
||||
:loading="saving"
|
||||
type="secondary"
|
||||
:text="$t('core.common.buttons.submit')"
|
||||
@submit="$formkit.submit('tag-form')"
|
||||
>
|
||||
</SubmitButton>
|
||||
<VButton @click="onVisibleChange(false)">取消 Esc</VButton>
|
||||
<VButton @click="onVisibleChange(false)">
|
||||
{{ $t("core.common.buttons.cancel_and_shortcut") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
</VModal>
|
||||
|
|
|
@ -3,6 +3,7 @@ import type { Tag } from "@halo-dev/api-client";
|
|||
import type { Ref } from "vue";
|
||||
import { Dialog, Toast } from "@halo-dev/components";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
interface usePostTagReturn {
|
||||
tags: Ref<Tag[] | undefined>;
|
||||
|
@ -12,6 +13,8 @@ interface usePostTagReturn {
|
|||
}
|
||||
|
||||
export function usePostTag(): usePostTagReturn {
|
||||
const { t } = useI18n();
|
||||
|
||||
const {
|
||||
data: tags,
|
||||
isLoading,
|
||||
|
@ -38,16 +41,18 @@ export function usePostTag(): usePostTagReturn {
|
|||
|
||||
const handleDelete = async (tag: Tag) => {
|
||||
Dialog.warning({
|
||||
title: "确定要删除该标签吗?",
|
||||
description: "删除此标签之后,对应文章的关联将被解除。该操作不可恢复。",
|
||||
title: t("core.post_tag.operations.delete.title"),
|
||||
description: t("core.post_tag.operations.delete.description"),
|
||||
confirmType: "danger",
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await apiClient.extension.tag.deletecontentHaloRunV1alpha1Tag({
|
||||
name: tag.metadata.name,
|
||||
});
|
||||
|
||||
Toast.success("删除成功");
|
||||
Toast.success(t("core.common.toast.delete_success"));
|
||||
} catch (e) {
|
||||
console.error("Failed to delete tag", e);
|
||||
} finally {
|
||||
|
|
|
@ -16,7 +16,9 @@ const dashboardStats = inject<Ref<DashboardStats>>("dashboardStats");
|
|||
</span>
|
||||
|
||||
<div>
|
||||
<span class="text-sm text-gray-500">文章</span>
|
||||
<span class="text-sm text-gray-500">
|
||||
{{ $t("core.dashboard.widgets.presets.post_stats.title") }}
|
||||
</span>
|
||||
<p class="text-2xl font-medium text-gray-900">
|
||||
{{ dashboardStats?.posts || 0 }}
|
||||
</p>
|
||||
|
|
|
@ -34,7 +34,7 @@ const { data } = useQuery<ListedPost[]>({
|
|||
<VCard
|
||||
:body-class="['h-full', '!p-0', 'overflow-y-auto']"
|
||||
class="h-full"
|
||||
title="最近文章"
|
||||
:title="$t('core.dashboard.widgets.presets.recent_published.title')"
|
||||
>
|
||||
<ul class="box-border h-full w-full divide-y divide-gray-100" role="list">
|
||||
<li v-for="(post, index) in data" :key="index">
|
||||
|
@ -50,10 +50,20 @@ const { data } = useQuery<ListedPost[]>({
|
|||
<template #description>
|
||||
<VSpace>
|
||||
<span class="text-xs text-gray-500">
|
||||
访问量 {{ post.stats.visit || 0 }}
|
||||
{{
|
||||
$t(
|
||||
"core.dashboard.widgets.presets.recent_published.visits",
|
||||
{ visits: post.stats.visit || 0 }
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500">
|
||||
评论 {{ post.stats.totalComment || 0 }}
|
||||
{{
|
||||
$t(
|
||||
"core.dashboard.widgets.presets.recent_published.comments",
|
||||
{ comments: post.stats.totalComment || 0 }
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</VSpace>
|
||||
</template>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<VPageHeader title="仪表盘">
|
||||
<VPageHeader :title="$t('core.dashboard.title')">
|
||||
<template #icon>
|
||||
<IconDashboard class="mr-2 self-center" />
|
||||
</template>
|
||||
|
@ -9,14 +9,18 @@
|
|||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
添加组件
|
||||
{{ $t("core.dashboard.actions.add_widget") }}
|
||||
</VButton>
|
||||
<VButton type="secondary" @click="settings = !settings">
|
||||
<template #icon>
|
||||
<IconSettings v-if="!settings" class="h-full w-full" />
|
||||
<IconSave v-else class="h-full w-full" />
|
||||
</template>
|
||||
{{ settings ? "完成" : "设置" }}
|
||||
{{
|
||||
settings
|
||||
? $t("core.dashboard.actions.done")
|
||||
: $t("core.dashboard.actions.setting")
|
||||
}}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
|
@ -59,7 +63,7 @@
|
|||
height="calc(100vh - 20px)"
|
||||
:width="1280"
|
||||
:layer-closable="true"
|
||||
title="小组件"
|
||||
:title="$t('core.dashboard.widgets.modal_title')"
|
||||
>
|
||||
<VTabbar
|
||||
v-model:active-id="activeId"
|
||||
|
@ -120,11 +124,14 @@ import { useStorage } from "@vueuse/core";
|
|||
import cloneDeep from "lodash.clonedeep";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import type { DashboardStats } from "@halo-dev/api-client/index";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const widgetsGroup = [
|
||||
{
|
||||
id: "post",
|
||||
label: "文章",
|
||||
label: t("core.dashboard.widgets.groups.post"),
|
||||
widgets: [
|
||||
{ x: 0, y: 0, w: 3, h: 3, i: 0, widget: "PostStatsWidget" },
|
||||
{ x: 0, y: 0, w: 6, h: 10, i: 1, widget: "RecentPublishedWidget" },
|
||||
|
@ -132,19 +139,19 @@ const widgetsGroup = [
|
|||
},
|
||||
{
|
||||
id: "page",
|
||||
label: "页面",
|
||||
label: t("core.dashboard.widgets.groups.page"),
|
||||
widgets: [
|
||||
{ x: 0, y: 0, w: 3, h: 3, i: 0, widget: "SinglePageStatsWidget" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "comment",
|
||||
label: "评论",
|
||||
label: t("core.dashboard.widgets.groups.comment"),
|
||||
widgets: [{ x: 0, y: 0, w: 3, h: 3, i: 0, widget: "CommentStatsWidget" }],
|
||||
},
|
||||
{
|
||||
id: "user",
|
||||
label: "用户",
|
||||
label: t("core.dashboard.widgets.groups.user"),
|
||||
widgets: [
|
||||
{ x: 0, y: 0, w: 3, h: 3, i: 0, widget: "UserStatsWidget" },
|
||||
{ x: 0, y: 0, w: 3, h: 3, i: 1, widget: "UserProfileWidget" },
|
||||
|
@ -152,7 +159,7 @@ const widgetsGroup = [
|
|||
},
|
||||
{
|
||||
id: "other",
|
||||
label: "其他",
|
||||
label: t("core.dashboard.widgets.groups.other"),
|
||||
widgets: [
|
||||
{ x: 0, y: 0, w: 3, h: 3, i: 0, widget: "ViewsStatsWidget" },
|
||||
{ x: 0, y: 0, w: 6, h: 10, i: 1, widget: "QuickLinkWidget" },
|
||||
|
|
|
@ -24,10 +24,10 @@ export default definePlugin({
|
|||
name: "Dashboard",
|
||||
component: Dashboard,
|
||||
meta: {
|
||||
title: "仪表盘",
|
||||
title: "core.dashboard.title",
|
||||
searchable: true,
|
||||
menu: {
|
||||
name: "仪表盘",
|
||||
name: "core.sidebar.menu.items.dashboard",
|
||||
group: "dashboard",
|
||||
icon: markRaw(IconDashboard),
|
||||
priority: 0,
|
||||
|
|
|
@ -18,6 +18,7 @@ import { markRaw, ref, type Component } from "vue";
|
|||
import { useRouter } from "vue-router";
|
||||
import ThemePreviewModal from "@/modules/interface/themes/components/preview/ThemePreviewModal.vue";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
interface Action {
|
||||
icon: Component;
|
||||
|
@ -30,10 +31,14 @@ const router = useRouter();
|
|||
|
||||
const themePreviewVisible = ref(false);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const actions: Action[] = [
|
||||
{
|
||||
icon: markRaw(IconUserLine),
|
||||
title: "个人资料",
|
||||
title: t(
|
||||
"core.dashboard.widgets.presets.quicklink.actions.user_profile.title"
|
||||
),
|
||||
action: () => {
|
||||
router.push({
|
||||
name: "UserDetail",
|
||||
|
@ -43,7 +48,9 @@ const actions: Action[] = [
|
|||
},
|
||||
{
|
||||
icon: markRaw(IconWindowLine),
|
||||
title: "查看站点",
|
||||
title: t(
|
||||
"core.dashboard.widgets.presets.quicklink.actions.view_site.title"
|
||||
),
|
||||
action: () => {
|
||||
themePreviewVisible.value = true;
|
||||
},
|
||||
|
@ -51,7 +58,7 @@ const actions: Action[] = [
|
|||
},
|
||||
{
|
||||
icon: markRaw(IconBookRead),
|
||||
title: "创建文章",
|
||||
title: t("core.dashboard.widgets.presets.quicklink.actions.new_post.title"),
|
||||
action: () => {
|
||||
router.push({
|
||||
name: "PostEditor",
|
||||
|
@ -61,7 +68,7 @@ const actions: Action[] = [
|
|||
},
|
||||
{
|
||||
icon: markRaw(IconPages),
|
||||
title: "创建页面",
|
||||
title: t("core.dashboard.widgets.presets.quicklink.actions.new_page.title"),
|
||||
action: () => {
|
||||
router.push({
|
||||
name: "SinglePageEditor",
|
||||
|
@ -71,7 +78,9 @@ const actions: Action[] = [
|
|||
},
|
||||
{
|
||||
icon: markRaw(IconFolder),
|
||||
title: "附件上传",
|
||||
title: t(
|
||||
"core.dashboard.widgets.presets.quicklink.actions.upload_attachment.title"
|
||||
),
|
||||
action: () => {
|
||||
router.push({
|
||||
name: "Attachments",
|
||||
|
@ -84,7 +93,9 @@ const actions: Action[] = [
|
|||
},
|
||||
{
|
||||
icon: markRaw(IconPalette),
|
||||
title: "主题管理",
|
||||
title: t(
|
||||
"core.dashboard.widgets.presets.quicklink.actions.theme_manage.title"
|
||||
),
|
||||
action: () => {
|
||||
router.push({
|
||||
name: "ThemeDetail",
|
||||
|
@ -94,7 +105,9 @@ const actions: Action[] = [
|
|||
},
|
||||
{
|
||||
icon: markRaw(IconPlug),
|
||||
title: "插件管理",
|
||||
title: t(
|
||||
"core.dashboard.widgets.presets.quicklink.actions.plugin_manage.title"
|
||||
),
|
||||
action: () => {
|
||||
router.push({
|
||||
name: "Plugins",
|
||||
|
@ -104,7 +117,7 @@ const actions: Action[] = [
|
|||
},
|
||||
{
|
||||
icon: markRaw(IconUserSettings),
|
||||
title: "新建用户",
|
||||
title: t("core.dashboard.widgets.presets.quicklink.actions.new_user.title"),
|
||||
action: () => {
|
||||
router.push({
|
||||
name: "Users",
|
||||
|
@ -117,14 +130,26 @@ const actions: Action[] = [
|
|||
},
|
||||
{
|
||||
icon: markRaw(IconSearch),
|
||||
title: "刷新搜索引擎",
|
||||
title: t(
|
||||
"core.dashboard.widgets.presets.quicklink.actions.refresh_search_engine.title"
|
||||
),
|
||||
action: () => {
|
||||
Dialog.warning({
|
||||
title: "确定要刷新搜索引擎索引吗?",
|
||||
description: "此操作会对所有已发布的文章重新创建搜索引擎索引。",
|
||||
title: t(
|
||||
"core.dashboard.widgets.presets.quicklink.actions.refresh_search_engine.dialog_title"
|
||||
),
|
||||
description: t(
|
||||
"core.dashboard.widgets.presets.quicklink.actions.refresh_search_engine.dialog_content"
|
||||
),
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
await apiClient.indices.buildPostIndices();
|
||||
Toast.success("刷新成功");
|
||||
Toast.success(
|
||||
t(
|
||||
"core.dashboard.widgets.presets.quicklink.actions.refresh_search_engine.success_message"
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
@ -136,7 +161,7 @@ const actions: Action[] = [
|
|||
<VCard
|
||||
:body-class="['h-full', 'overflow-y-auto', '@container']"
|
||||
class="h-full"
|
||||
title="快捷访问"
|
||||
:title="$t('core.dashboard.widgets.presets.quicklink.title')"
|
||||
>
|
||||
<div
|
||||
class="grid grid-cols-1 gap-2 overflow-hidden @sm:grid-cols-2 @md:grid-cols-3"
|
||||
|
@ -169,5 +194,10 @@ const actions: Action[] = [
|
|||
</div>
|
||||
</div>
|
||||
</VCard>
|
||||
<ThemePreviewModal v-model:visible="themePreviewVisible" title="查看站点" />
|
||||
<ThemePreviewModal
|
||||
v-model:visible="themePreviewVisible"
|
||||
:title="
|
||||
$t('core.dashboard.widgets.presets.quicklink.actions.view_site.title')
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
@ -16,7 +16,9 @@ const dashboardStats = inject<Ref<DashboardStats>>("dashboardStats");
|
|||
</span>
|
||||
|
||||
<div>
|
||||
<span class="text-sm text-gray-500">浏览量</span>
|
||||
<span class="text-sm text-gray-500">
|
||||
{{ $t("core.dashboard.widgets.presets.views_stats.title") }}
|
||||
</span>
|
||||
<p class="text-2xl font-medium text-gray-900">
|
||||
{{ dashboardStats?.visits || 0 }}
|
||||
</p>
|
||||
|
|
|
@ -28,6 +28,9 @@ import {
|
|||
} from "./utils";
|
||||
import { useDebounceFn } from "@vueuse/core";
|
||||
import { onBeforeRouteLeave } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const menuItems = ref<MenuItem[]>([] as MenuItem[]);
|
||||
const menuTreeItems = ref<MenuTreeItem[]>([] as MenuTreeItem[]);
|
||||
|
@ -154,9 +157,11 @@ const handleUpdateInBatch = useDebounceFn(async () => {
|
|||
|
||||
const handleDelete = async (menuItem: MenuTreeItem) => {
|
||||
Dialog.info({
|
||||
title: "确定要删除该菜单项吗?",
|
||||
description: "将同时删除所有子菜单项,删除后将无法恢复",
|
||||
title: t("core.menu.operations.delete_menu_item.title"),
|
||||
description: t("core.menu.operations.delete_menu_item.description"),
|
||||
confirmType: "danger",
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
await apiClient.extension.menuItem.deletev1alpha1MenuItem({
|
||||
name: menuItem.metadata.name,
|
||||
|
@ -175,7 +180,7 @@ const handleDelete = async (menuItem: MenuTreeItem) => {
|
|||
|
||||
await handleFetchMenuItems();
|
||||
|
||||
Toast.success("删除成功");
|
||||
Toast.success(t("core.common.toast.delete_success"));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -210,7 +215,7 @@ const handleResetMenuItems = async () => {
|
|||
@close="onMenuItemEditingModalClose"
|
||||
@saved="onMenuItemSaved"
|
||||
/>
|
||||
<VPageHeader title="菜单">
|
||||
<VPageHeader :title="$t('core.menu.title')">
|
||||
<template #icon>
|
||||
<IconListSettings class="mr-2 self-center" />
|
||||
</template>
|
||||
|
@ -244,7 +249,7 @@ const handleResetMenuItems = async () => {
|
|||
type="default"
|
||||
@click="menuItemEditingModal = true"
|
||||
>
|
||||
新增
|
||||
{{ $t("core.common.buttons.new") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</div>
|
||||
|
@ -254,12 +259,14 @@ const handleResetMenuItems = async () => {
|
|||
<VLoading v-if="loading" />
|
||||
<Transition v-else-if="!menuItems.length" appear name="fade">
|
||||
<VEmpty
|
||||
message="你可以尝试刷新或者新建菜单项"
|
||||
title="当前没有菜单项"
|
||||
:message="$t('core.menu.menu_item_empty.message')"
|
||||
:title="$t('core.menu.menu_item_empty.title')"
|
||||
>
|
||||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton @click="handleFetchMenuItems()"> 刷新</VButton>
|
||||
<VButton @click="handleFetchMenuItems()">
|
||||
{{ $t("core.common.buttons.refresh") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-permission="['system:menus:manage']"
|
||||
type="primary"
|
||||
|
@ -268,7 +275,7 @@ const handleResetMenuItems = async () => {
|
|||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
新增菜单项
|
||||
{{ $t("core.common.buttons.new") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
|
|
|
@ -7,6 +7,7 @@ import { apiClient } from "@/utils/api-client";
|
|||
import { reset } from "@formkit/core";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
import { setFocus } from "@/formkit/utils/focus";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -25,6 +26,8 @@ const emit = defineEmits<{
|
|||
(event: "created", menu: Menu): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const initialFormState: Menu = {
|
||||
spec: {
|
||||
displayName: "",
|
||||
|
@ -46,7 +49,9 @@ const isUpdateMode = computed(() => {
|
|||
});
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
return isUpdateMode.value ? "编辑菜单" : "新增菜单";
|
||||
return isUpdateMode.value
|
||||
? t("core.menu.menu_editing_modal.titles.update")
|
||||
: t("core.menu.menu_editing_modal.titles.create");
|
||||
});
|
||||
|
||||
const handleCreateMenu = async () => {
|
||||
|
@ -65,7 +70,7 @@ const handleCreateMenu = async () => {
|
|||
}
|
||||
onVisibleChange(false);
|
||||
|
||||
Toast.success("保存成功");
|
||||
Toast.success(t("core.common.toast.save_success"));
|
||||
} catch (e) {
|
||||
console.error("Failed to create menu", e);
|
||||
} finally {
|
||||
|
@ -124,7 +129,7 @@ watch(
|
|||
<FormKit
|
||||
id="menuDisplayNameInput"
|
||||
v-model="formState.spec.displayName"
|
||||
label="菜单名称"
|
||||
:label="$t('core.menu.menu_editing_modal.fields.display_name.label')"
|
||||
type="text"
|
||||
name="displayName"
|
||||
validation="required|length:0,100"
|
||||
|
@ -136,10 +141,13 @@ watch(
|
|||
v-if="visible"
|
||||
:loading="saving"
|
||||
type="secondary"
|
||||
:text="$t('core.common.buttons.submit')"
|
||||
@submit="$formkit.submit('menu-form')"
|
||||
>
|
||||
</SubmitButton>
|
||||
<VButton @click="onVisibleChange(false)">取消 Esc</VButton>
|
||||
<VButton @click="onVisibleChange(false)">
|
||||
{{ $t("core.common.buttons.cancel_and_shortcut") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
</VModal>
|
||||
|
|
|
@ -8,6 +8,7 @@ import { reset } from "@formkit/core";
|
|||
import cloneDeep from "lodash.clonedeep";
|
||||
import { setFocus } from "@/formkit/utils/focus";
|
||||
import AnnotationsForm from "@/components/form/AnnotationsForm.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -30,6 +31,8 @@ const emit = defineEmits<{
|
|||
(event: "saved", menuItem: MenuItem): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const initialFormState: MenuItem = {
|
||||
spec: {
|
||||
displayName: "",
|
||||
|
@ -55,7 +58,9 @@ const isUpdateMode = computed(() => {
|
|||
});
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
return isUpdateMode.value ? "编辑菜单项" : "新增菜单项";
|
||||
return isUpdateMode.value
|
||||
? t("core.menu.menu_item_editing_modal.titles.update")
|
||||
: t("core.menu.menu_item_editing_modal.titles.create");
|
||||
});
|
||||
|
||||
const annotationsFormRef = ref<InstanceType<typeof AnnotationsForm>>();
|
||||
|
@ -128,7 +133,7 @@ const handleSaveMenuItem = async () => {
|
|||
emit("saved", data);
|
||||
}
|
||||
|
||||
Toast.success("保存成功");
|
||||
Toast.success(t("core.common.toast.save_success"));
|
||||
} catch (e) {
|
||||
console.error("Failed to create menu item", e);
|
||||
} finally {
|
||||
|
@ -202,10 +207,12 @@ const baseRef: Ref = {
|
|||
|
||||
const menuItemRefs: MenuItemRef[] = [
|
||||
{
|
||||
label: "自定义链接",
|
||||
label: t(
|
||||
"core.menu.menu_item_editing_modal.fields.ref_kind.options.custom"
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "文章",
|
||||
label: t("core.menu.menu_item_editing_modal.fields.ref_kind.options.post"),
|
||||
inputType: "postSelect",
|
||||
ref: {
|
||||
...baseRef,
|
||||
|
@ -213,7 +220,9 @@ const menuItemRefs: MenuItemRef[] = [
|
|||
},
|
||||
},
|
||||
{
|
||||
label: "自定义页面",
|
||||
label: t(
|
||||
"core.menu.menu_item_editing_modal.fields.ref_kind.options.single_page"
|
||||
),
|
||||
inputType: "singlePageSelect",
|
||||
ref: {
|
||||
...baseRef,
|
||||
|
@ -221,7 +230,9 @@ const menuItemRefs: MenuItemRef[] = [
|
|||
},
|
||||
},
|
||||
{
|
||||
label: "分类",
|
||||
label: t(
|
||||
"core.menu.menu_item_editing_modal.fields.ref_kind.options.category"
|
||||
),
|
||||
inputType: "categorySelect",
|
||||
ref: {
|
||||
...baseRef,
|
||||
|
@ -229,7 +240,7 @@ const menuItemRefs: MenuItemRef[] = [
|
|||
},
|
||||
},
|
||||
{
|
||||
label: "标签",
|
||||
label: t("core.menu.menu_item_editing_modal.fields.ref_kind.options.tag"),
|
||||
inputType: "tagSelect",
|
||||
ref: {
|
||||
...baseRef,
|
||||
|
@ -277,15 +288,23 @@ const onMenuItemSourceChange = () => {
|
|||
<div class="md:grid md:grid-cols-4 md:gap-6">
|
||||
<div class="md:col-span-1">
|
||||
<div class="sticky top-0">
|
||||
<span class="text-base font-medium text-gray-900"> 常规 </span>
|
||||
<span class="text-base font-medium text-gray-900">
|
||||
{{ $t("core.menu.menu_item_editing_modal.groups.general") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0">
|
||||
<FormKit
|
||||
v-if="!isUpdateMode && menu && visible"
|
||||
v-model="selectedParentMenuItem"
|
||||
label="上级菜单项"
|
||||
placeholder="选择上级菜单项"
|
||||
:label="
|
||||
$t('core.menu.menu_item_editing_modal.fields.parent.label')
|
||||
"
|
||||
:placeholder="
|
||||
$t(
|
||||
'core.menu.menu_item_editing_modal.fields.parent.placeholder'
|
||||
)
|
||||
"
|
||||
type="menuItemSelect"
|
||||
:menu-items="menu?.spec.menuItems || []"
|
||||
/>
|
||||
|
@ -294,7 +313,9 @@ const onMenuItemSourceChange = () => {
|
|||
v-model="selectedRefKind"
|
||||
:options="menuItemRefsMap"
|
||||
:disabled="isUpdateMode"
|
||||
label="类型"
|
||||
:label="
|
||||
$t('core.menu.menu_item_editing_modal.fields.ref_kind.label')
|
||||
"
|
||||
type="select"
|
||||
@change="onMenuItemSourceChange"
|
||||
/>
|
||||
|
@ -303,7 +324,11 @@ const onMenuItemSourceChange = () => {
|
|||
v-if="!selectedRefKind"
|
||||
id="displayNameInput"
|
||||
v-model="formState.spec.displayName"
|
||||
label="名称"
|
||||
:label="
|
||||
$t(
|
||||
'core.menu.menu_item_editing_modal.fields.display_name.label'
|
||||
)
|
||||
"
|
||||
type="text"
|
||||
name="displayName"
|
||||
validation="required|length:0,100"
|
||||
|
@ -312,7 +337,7 @@ const onMenuItemSourceChange = () => {
|
|||
<FormKit
|
||||
v-if="!selectedRefKind"
|
||||
v-model="formState.spec.href"
|
||||
label="链接地址"
|
||||
:label="$t('core.menu.menu_item_editing_modal.fields.href.label')"
|
||||
type="text"
|
||||
name="href"
|
||||
validation="required|length:0,1024"
|
||||
|
@ -323,7 +348,12 @@ const onMenuItemSourceChange = () => {
|
|||
:id="selectedRef.inputType"
|
||||
:key="selectedRef.inputType"
|
||||
v-model="selectedRefName"
|
||||
:placeholder="`请选择${selectedRef.label}`"
|
||||
:placeholder="
|
||||
$t(
|
||||
'core.menu.menu_item_editing_modal.fields.ref_kind.placeholder',
|
||||
{ label: selectedRef.label }
|
||||
)
|
||||
"
|
||||
:label="selectedRef.label"
|
||||
:type="selectedRef.inputType"
|
||||
validation="required"
|
||||
|
@ -331,24 +361,34 @@ const onMenuItemSourceChange = () => {
|
|||
|
||||
<FormKit
|
||||
v-model="formState.spec.target"
|
||||
label="打开方式"
|
||||
:label="
|
||||
$t('core.menu.menu_item_editing_modal.fields.target.label')
|
||||
"
|
||||
type="select"
|
||||
name="target"
|
||||
:options="[
|
||||
{
|
||||
label: '当前窗口',
|
||||
label: $t(
|
||||
'core.menu.menu_item_editing_modal.fields.target.options.self'
|
||||
),
|
||||
value: '_self',
|
||||
},
|
||||
{
|
||||
label: '新窗口',
|
||||
label: $t(
|
||||
'core.menu.menu_item_editing_modal.fields.target.options.blank'
|
||||
),
|
||||
value: '_blank',
|
||||
},
|
||||
{
|
||||
label: '父窗口',
|
||||
label: $t(
|
||||
'core.menu.menu_item_editing_modal.fields.target.options.parent'
|
||||
),
|
||||
value: '_parent',
|
||||
},
|
||||
{
|
||||
label: '顶级窗口',
|
||||
label: $t(
|
||||
'core.menu.menu_item_editing_modal.fields.target.options.top'
|
||||
),
|
||||
value: '_top',
|
||||
},
|
||||
]"
|
||||
|
@ -365,7 +405,9 @@ const onMenuItemSourceChange = () => {
|
|||
<div class="md:grid md:grid-cols-4 md:gap-6">
|
||||
<div class="md:col-span-1">
|
||||
<div class="sticky top-0">
|
||||
<span class="text-base font-medium text-gray-900"> 元数据 </span>
|
||||
<span class="text-base font-medium text-gray-900">
|
||||
{{ $t("core.menu.menu_item_editing_modal.groups.annotations") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0">
|
||||
|
@ -385,10 +427,13 @@ const onMenuItemSourceChange = () => {
|
|||
v-if="visible"
|
||||
:loading="saving"
|
||||
type="secondary"
|
||||
:text="$t('core.common.buttons.submit')"
|
||||
@submit="$formkit.submit('menuitem-form')"
|
||||
>
|
||||
</SubmitButton>
|
||||
<VButton @click="onVisibleChange(false)">取消 Esc</VButton>
|
||||
<VButton @click="onVisibleChange(false)">
|
||||
{{ $t("core.common.buttons.cancel_and_shortcut") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
</VModal>
|
||||
|
|
|
@ -11,8 +11,10 @@ import Draggable from "vuedraggable";
|
|||
import { ref } from "vue";
|
||||
import type { MenuTreeItem } from "@/modules/interface/menus/utils";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
const { t } = useI18n();
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
|
@ -49,10 +51,14 @@ function onDelete(menuItem: MenuTreeItem) {
|
|||
}
|
||||
|
||||
const TargetRef = {
|
||||
Post: "文章",
|
||||
SinglePage: "页面",
|
||||
Category: "分类",
|
||||
Tag: "标签",
|
||||
Post: t("core.menu.menu_item_editing_modal.fields.ref_kind.options.post"),
|
||||
SinglePage: t(
|
||||
"core.menu.menu_item_editing_modal.fields.ref_kind.options.single_page"
|
||||
),
|
||||
Category: t(
|
||||
"core.menu.menu_item_editing_modal.fields.ref_kind.options.category"
|
||||
),
|
||||
Tag: t("core.menu.menu_item_editing_modal.fields.ref_kind.options.tag"),
|
||||
};
|
||||
|
||||
function getMenuItemRefDisplayName(menuItem: MenuTreeItem) {
|
||||
|
@ -112,7 +118,11 @@ function getMenuItemRefDisplayName(menuItem: MenuTreeItem) {
|
|||
<template #end>
|
||||
<VEntityField v-if="menuItem.metadata.deletionTimestamp">
|
||||
<template #description>
|
||||
<VStatusDot v-tooltip="`删除中`" state="warning" animate />
|
||||
<VStatusDot
|
||||
v-tooltip="$t('core.common.status.deleting')"
|
||||
state="warning"
|
||||
animate
|
||||
/>
|
||||
</template>
|
||||
</VEntityField>
|
||||
</template>
|
||||
|
@ -126,7 +136,7 @@ function getMenuItemRefDisplayName(menuItem: MenuTreeItem) {
|
|||
type="secondary"
|
||||
@click="onOpenEditingModal(menuItem)"
|
||||
>
|
||||
修改
|
||||
{{ $t("core.common.buttons.edit") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-close-popper
|
||||
|
@ -134,7 +144,7 @@ function getMenuItemRefDisplayName(menuItem: MenuTreeItem) {
|
|||
type="default"
|
||||
@click="onOpenCreateByParentModal(menuItem)"
|
||||
>
|
||||
添加子菜单项
|
||||
{{ $t("core.menu.operations.add_sub_menu_item.button") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-close-popper
|
||||
|
@ -142,7 +152,7 @@ function getMenuItemRefDisplayName(menuItem: MenuTreeItem) {
|
|||
type="danger"
|
||||
@click="onDelete(menuItem)"
|
||||
>
|
||||
删除
|
||||
{{ $t("core.common.buttons.delete") }}
|
||||
</VButton>
|
||||
</template>
|
||||
</VEntity>
|
||||
|
|
|
@ -19,8 +19,10 @@ import { apiClient } from "@/utils/api-client";
|
|||
import { useRouteQuery } from "@vueuse/router";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
import { onBeforeRouteLeave } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -97,9 +99,11 @@ const handleSelect = (menu: Menu) => {
|
|||
|
||||
const handleDeleteMenu = async (menu: Menu) => {
|
||||
Dialog.warning({
|
||||
title: "确定要删除该菜单吗?",
|
||||
description: "将同时删除该菜单下的所有菜单项,该操作不可恢复。",
|
||||
title: t("core.menu.operations.delete_menu.title"),
|
||||
description: t("core.menu.operations.delete_menu.description"),
|
||||
confirmType: "danger",
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await apiClient.extension.menu.deletev1alpha1Menu({
|
||||
|
@ -115,7 +119,7 @@ const handleDeleteMenu = async (menu: Menu) => {
|
|||
|
||||
await Promise.all(deleteItemsPromises);
|
||||
|
||||
Toast.success("删除成功");
|
||||
Toast.success(t("core.common.toast.delete_success"));
|
||||
} catch (e) {
|
||||
console.error("Failed to delete menu", e);
|
||||
} finally {
|
||||
|
@ -185,7 +189,7 @@ const handleSetPrimaryMenu = async (menu: Menu) => {
|
|||
}
|
||||
await handleFetchPrimaryMenuName();
|
||||
|
||||
Toast.success("设置成功");
|
||||
Toast.success(t("core.menu.operations.set_primary.toast_success"));
|
||||
};
|
||||
|
||||
onMounted(handleFetchPrimaryMenuName);
|
||||
|
@ -197,13 +201,18 @@ onMounted(handleFetchPrimaryMenuName);
|
|||
@close="handleFetchMenus()"
|
||||
@created="handleSelect"
|
||||
/>
|
||||
<VCard :body-class="['!p-0']" title="菜单">
|
||||
<VCard :body-class="['!p-0']" :title="$t('core.menu.title')">
|
||||
<VLoading v-if="loading" />
|
||||
<Transition v-else-if="!menus.length" appear name="fade">
|
||||
<VEmpty message="你可以尝试刷新或者新建菜单" title="当前没有菜单">
|
||||
<VEmpty
|
||||
:message="$t('core.menu.empty.message')"
|
||||
:title="$t('core.menu.empty.title')"
|
||||
>
|
||||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton size="sm" @click="handleFetchMenus()"> 刷新</VButton>
|
||||
<VButton size="sm" @click="handleFetchMenus()">
|
||||
{{ $t("core.common.buttons.refresh") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
</VEmpty>
|
||||
|
@ -221,17 +230,27 @@ onMounted(handleFetchPrimaryMenuName);
|
|||
<template #start>
|
||||
<VEntityField
|
||||
:title="menu.spec?.displayName"
|
||||
:description="`${menu.spec.menuItems?.length || 0} 个菜单项`"
|
||||
:description="
|
||||
$t('core.menu.list.fields.items_count', {
|
||||
count: menu.spec.menuItems?.length || 0,
|
||||
})
|
||||
"
|
||||
>
|
||||
<template v-if="menu.metadata.name === primaryMenuName" #extra>
|
||||
<VTag>主菜单</VTag>
|
||||
<VTag>
|
||||
{{ $t("core.menu.list.fields.primary") }}
|
||||
</VTag>
|
||||
</template>
|
||||
</VEntityField>
|
||||
</template>
|
||||
<template #end>
|
||||
<VEntityField v-if="menu.metadata.deletionTimestamp">
|
||||
<template #description>
|
||||
<VStatusDot v-tooltip="`删除中`" state="warning" animate />
|
||||
<VStatusDot
|
||||
v-tooltip="$t('core.common.status.deleting')"
|
||||
state="warning"
|
||||
animate
|
||||
/>
|
||||
</template>
|
||||
</VEntityField>
|
||||
</template>
|
||||
|
@ -245,7 +264,7 @@ onMounted(handleFetchPrimaryMenuName);
|
|||
type="secondary"
|
||||
@click="handleSetPrimaryMenu(menu)"
|
||||
>
|
||||
设置为主菜单
|
||||
{{ $t("core.menu.operations.set_primary.button") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-close-popper
|
||||
|
@ -253,7 +272,7 @@ onMounted(handleFetchPrimaryMenuName);
|
|||
type="default"
|
||||
@click="handleOpenEditingModal(menu)"
|
||||
>
|
||||
修改
|
||||
{{ $t("core.common.buttons.edit") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-close-popper
|
||||
|
@ -261,7 +280,7 @@ onMounted(handleFetchPrimaryMenuName);
|
|||
type="danger"
|
||||
@click="handleDeleteMenu(menu)"
|
||||
>
|
||||
删除
|
||||
{{ $t("core.common.buttons.delete") }}
|
||||
</VButton>
|
||||
</template>
|
||||
</VEntity>
|
||||
|
@ -270,7 +289,7 @@ onMounted(handleFetchPrimaryMenuName);
|
|||
</Transition>
|
||||
<template v-if="currentUserHasPermission(['system:menus:manage'])" #footer>
|
||||
<VButton block type="secondary" @click="handleOpenEditingModal()">
|
||||
新增
|
||||
{{ $t("core.common.buttons.new") }}
|
||||
</VButton>
|
||||
</template>
|
||||
</VCard>
|
||||
|
|
|
@ -16,11 +16,11 @@ export default definePlugin({
|
|||
name: "Menus",
|
||||
component: Menus,
|
||||
meta: {
|
||||
title: "菜单",
|
||||
title: "core.menu.title",
|
||||
searchable: true,
|
||||
permissions: ["system:menus:view"],
|
||||
menu: {
|
||||
name: "菜单",
|
||||
name: "core.sidebar.menu.items.menus",
|
||||
group: "interface",
|
||||
icon: markRaw(IconListSettings),
|
||||
priority: 1,
|
||||
|
|
|
@ -23,6 +23,9 @@ import type { Ref } from "vue";
|
|||
import type { Theme } from "@halo-dev/api-client";
|
||||
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const selectedTheme = inject<Ref<Theme | undefined>>("selectedTheme", ref());
|
||||
const upgradeModal = ref(false);
|
||||
|
@ -32,8 +35,10 @@ const { isActivated, getFailedMessage, handleResetSettingConfig } =
|
|||
|
||||
const handleReloadTheme = async () => {
|
||||
Dialog.warning({
|
||||
title: "确定要重载主题的所有配置吗?",
|
||||
description: "该操作仅会重载主题配置和设置表单定义,不会删除已保存的配置。",
|
||||
title: t("core.theme.operations.reload.title"),
|
||||
description: t("core.theme.operations.reload.description"),
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
if (!selectedTheme?.value) {
|
||||
|
@ -44,7 +49,7 @@ const handleReloadTheme = async () => {
|
|||
name: selectedTheme.value.metadata.name as string,
|
||||
});
|
||||
|
||||
Toast.success("重载配置成功");
|
||||
Toast.success(t("core.theme.operations.reload.toast_success"));
|
||||
|
||||
window.location.reload();
|
||||
} catch (e) {
|
||||
|
@ -82,7 +87,11 @@ const onUpgradeModalClose = () => {
|
|||
{{ selectedTheme?.spec.version }}
|
||||
</span>
|
||||
<VTag>
|
||||
{{ isActivated ? "当前启用" : "未启用" }}
|
||||
{{
|
||||
isActivated
|
||||
? t("core.common.status.activated")
|
||||
: t("core.common.status.not_activated")
|
||||
}}
|
||||
</VTag>
|
||||
<VStatusDot
|
||||
v-if="getFailedMessage()"
|
||||
|
@ -108,7 +117,7 @@ const onUpgradeModalClose = () => {
|
|||
type="secondary"
|
||||
@click="upgradeModal = true"
|
||||
>
|
||||
升级
|
||||
{{ $t("core.common.buttons.upgrade") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-close-popper
|
||||
|
@ -116,7 +125,7 @@ const onUpgradeModalClose = () => {
|
|||
type="default"
|
||||
@click="handleReloadTheme"
|
||||
>
|
||||
重载主题配置
|
||||
{{ $t("core.theme.operations.reload.button") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-close-popper
|
||||
|
@ -124,7 +133,7 @@ const onUpgradeModalClose = () => {
|
|||
type="danger"
|
||||
@click="handleResetSettingConfig"
|
||||
>
|
||||
重置
|
||||
{{ $t("core.common.buttons.reset") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</div>
|
||||
|
@ -145,7 +154,9 @@ const onUpgradeModalClose = () => {
|
|||
<div
|
||||
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
|
||||
>
|
||||
<dt class="text-sm font-medium text-gray-900">作者</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.theme.detail.fields.author") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
|
||||
{{ selectedTheme?.spec.author.name }}
|
||||
</dd>
|
||||
|
@ -153,7 +164,9 @@ const onUpgradeModalClose = () => {
|
|||
<div
|
||||
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
|
||||
>
|
||||
<dt class="text-sm font-medium text-gray-900">网站</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.theme.detail.fields.website") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
|
||||
<a
|
||||
:href="selectedTheme?.spec.website"
|
||||
|
@ -167,7 +180,9 @@ const onUpgradeModalClose = () => {
|
|||
<div
|
||||
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
|
||||
>
|
||||
<dt class="text-sm font-medium text-gray-900">源码仓库</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.theme.detail.fields.repo") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
|
||||
<a
|
||||
:href="selectedTheme?.spec.repo"
|
||||
|
@ -181,7 +196,9 @@ const onUpgradeModalClose = () => {
|
|||
<div
|
||||
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
|
||||
>
|
||||
<dt class="text-sm font-medium text-gray-900">当前版本</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.theme.detail.fields.version") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
|
||||
{{ selectedTheme?.spec.version }}
|
||||
</dd>
|
||||
|
@ -189,7 +206,9 @@ const onUpgradeModalClose = () => {
|
|||
<div
|
||||
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
|
||||
>
|
||||
<dt class="text-sm font-medium text-gray-900">Halo 版本要求</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.theme.detail.fields.requires") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
|
||||
{{ selectedTheme?.spec.requires }}
|
||||
</dd>
|
||||
|
@ -197,7 +216,9 @@ const onUpgradeModalClose = () => {
|
|||
<div
|
||||
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
|
||||
>
|
||||
<dt class="text-sm font-medium text-gray-900">存储位置</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.theme.detail.fields.storage_location") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
|
||||
{{ selectedTheme?.status?.location }}
|
||||
</dd>
|
||||
|
@ -207,7 +228,9 @@ const onUpgradeModalClose = () => {
|
|||
v-if="false"
|
||||
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
|
||||
>
|
||||
<dt class="text-sm font-medium text-gray-900">插件依赖</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.theme.detail.fields.plugin_requires") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm sm:col-span-3 sm:mt-0">
|
||||
<VAlert
|
||||
description="当前有 1 个插件还未安装"
|
||||
|
@ -229,7 +252,7 @@ const onUpgradeModalClose = () => {
|
|||
</RouterLink>
|
||||
<div class="text-xs">
|
||||
<VSpace>
|
||||
<VTag> 已安装</VTag>
|
||||
<VTag>{{ $t("core.common.status.installed") }}</VTag>
|
||||
</VSpace>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -243,7 +266,9 @@ const onUpgradeModalClose = () => {
|
|||
</span>
|
||||
<div class="text-xs">
|
||||
<VSpace>
|
||||
<VTag>未安装</VTag>
|
||||
<VTag>
|
||||
{{ $t("core.common.status.not_installed") }}
|
||||
</VTag>
|
||||
</VSpace>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -13,6 +13,9 @@ import type { ConfigMap, Setting, Theme } from "@halo-dev/api-client";
|
|||
import { useRouteParams } from "@vueuse/router";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { useSettingFormConvert } from "@/composables/use-setting-form";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const group = useRouteParams<string>("group");
|
||||
|
||||
|
@ -63,7 +66,7 @@ const handleSaveConfigMap = async () => {
|
|||
configMap: configMapToUpdate,
|
||||
});
|
||||
|
||||
Toast.success("保存成功");
|
||||
Toast.success(t("core.common.toast.save_success"));
|
||||
|
||||
await handleFetchSettings();
|
||||
configMap.value = newConfigMap;
|
||||
|
@ -109,7 +112,7 @@ watch(
|
|||
type="secondary"
|
||||
@click="$formkit.submit(group || '')"
|
||||
>
|
||||
保存
|
||||
{{ $t("core.common.buttons.save") }}
|
||||
</VButton>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -21,6 +21,9 @@ import { computed, ref, watch } from "vue";
|
|||
import type { Theme } from "@halo-dev/api-client";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { onBeforeRouteLeave } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -48,7 +51,9 @@ const creating = ref(false);
|
|||
const refreshInterval = ref();
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
return activeTab.value === "installed" ? "已安装的主题" : "未安装的主题";
|
||||
return activeTab.value === "installed"
|
||||
? t("core.theme.list_modal.titles.installed_themes")
|
||||
: t("core.theme.list_modal.titles.not_installed_themes");
|
||||
});
|
||||
|
||||
const handleFetchThemes = async (options?: { mute?: boolean }) => {
|
||||
|
@ -110,7 +115,7 @@ const handleCreateTheme = async (theme: Theme) => {
|
|||
|
||||
activeTab.value = "installed";
|
||||
|
||||
Toast.success("安装成功");
|
||||
Toast.success(t("core.theme.list_modal.operations.install.toast_success"));
|
||||
} catch (error) {
|
||||
console.error("Failed to create theme", error);
|
||||
} finally {
|
||||
|
@ -183,17 +188,21 @@ const handleOpenInstallModal = () => {
|
|||
type="outline"
|
||||
class="my-[12px] mx-[16px]"
|
||||
>
|
||||
<VTabItem id="installed" label="已安装" class="-mx-[16px]">
|
||||
<VTabItem
|
||||
id="installed"
|
||||
:label="$t('core.theme.list_modal.tabs.installed')"
|
||||
class="-mx-[16px]"
|
||||
>
|
||||
<VLoading v-if="loading" />
|
||||
<Transition v-else-if="!themes.length" appear name="fade">
|
||||
<VEmpty
|
||||
message="当前没有已安装的主题,你可以尝试刷新或者安装新主题"
|
||||
title="当前没有已安装的主题"
|
||||
:message="$t('core.theme.list_modal.empty.message')"
|
||||
:title="$t('core.theme.list_modal.empty.title')"
|
||||
>
|
||||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton :loading="loading" @click="handleFetchThemes()">
|
||||
刷新
|
||||
{{ $t("core.common.buttons.refresh") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-permission="['system:themes:manage']"
|
||||
|
@ -203,7 +212,7 @@ const handleOpenInstallModal = () => {
|
|||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
安装主题
|
||||
{{ $t("core.theme.common.buttons.install") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
|
@ -232,14 +241,20 @@ const handleOpenInstallModal = () => {
|
|||
</ul>
|
||||
</Transition>
|
||||
</VTabItem>
|
||||
<VTabItem id="uninstalled" label="未安装" class="-mx-[16px]">
|
||||
<VTabItem
|
||||
id="uninstalled"
|
||||
:label="$t('core.theme.list_modal.tabs.not_installed')"
|
||||
class="-mx-[16px]"
|
||||
>
|
||||
<VLoading v-if="loading" />
|
||||
<Transition v-else-if="!themes.length" appear name="fade">
|
||||
<VEmpty title="当前没有未安装的主题">
|
||||
<VEmpty
|
||||
:title="$t('core.theme.list_modal.not_installed_empty.title')"
|
||||
>
|
||||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton :loading="loading" @click="handleFetchThemes">
|
||||
刷新
|
||||
{{ $t("core.common.buttons.refresh") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
|
@ -269,18 +284,18 @@ const handleOpenInstallModal = () => {
|
|||
<div
|
||||
class="flex h-full items-center justify-center object-cover"
|
||||
>
|
||||
<span class="text-xs text-gray-400"
|
||||
>加载中...</span
|
||||
>
|
||||
<span class="text-xs text-gray-400">
|
||||
{{ $t("core.common.status.loading") }}...
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #error>
|
||||
<div
|
||||
class="flex h-full items-center justify-center object-cover"
|
||||
>
|
||||
<span class="text-xs text-red-400"
|
||||
>加载异常</span
|
||||
>
|
||||
<span class="text-xs text-red-400">
|
||||
{{ $t("core.common.status.loading_error") }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</LazyImage>
|
||||
|
@ -325,7 +340,7 @@ const handleOpenInstallModal = () => {
|
|||
:disabled="creating"
|
||||
@click="handleCreateTheme(theme)"
|
||||
>
|
||||
安装
|
||||
{{ $t("core.common.buttons.install") }}
|
||||
</VButton>
|
||||
</template>
|
||||
</VEntityField>
|
||||
|
@ -344,9 +359,11 @@ const handleOpenInstallModal = () => {
|
|||
type="secondary"
|
||||
@click="handleOpenInstallModal()"
|
||||
>
|
||||
安装主题
|
||||
{{ $t("core.theme.common.buttons.install") }}
|
||||
</VButton>
|
||||
<VButton @click="onVisibleChange(false)">
|
||||
{{ $t("core.common.buttons.close") }}
|
||||
</VButton>
|
||||
<VButton @click="onVisibleChange(false)">关闭</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
</VModal>
|
||||
|
|
|
@ -3,6 +3,9 @@ import { VModal } from "@halo-dev/components";
|
|||
import UppyUpload from "@/components/upload/UppyUpload.vue";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import type { Theme } from "@halo-dev/api-client";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -24,8 +27,10 @@ const uploadVisible = ref(false);
|
|||
|
||||
const modalTitle = computed(() => {
|
||||
return props.upgradeTheme
|
||||
? `升级主题(${props.upgradeTheme.spec.displayName})`
|
||||
: "安装主题";
|
||||
? t("core.theme.upload_modal.titles.upgrade", {
|
||||
display_name: props.upgradeTheme.spec.displayName,
|
||||
})
|
||||
: t("core.theme.upload_modal.titles.install");
|
||||
});
|
||||
|
||||
const handleVisibleChange = (visible: boolean) => {
|
||||
|
|
|
@ -16,8 +16,10 @@ import { apiClient } from "@/utils/api-client";
|
|||
import { toRefs } from "vue";
|
||||
import { useThemeLifeCycle } from "../../composables/use-theme";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -48,10 +50,12 @@ const handleUninstall = async (theme: Theme, deleteExtensions?: boolean) => {
|
|||
Dialog.warning({
|
||||
title: `${
|
||||
deleteExtensions
|
||||
? "确定要卸载该主题以及对应的配置吗?"
|
||||
: "确定要卸载该主题吗?"
|
||||
? t("core.theme.operations.uninstall_and_delete_config.title")
|
||||
: t("core.theme.operations.uninstall.title")
|
||||
}`,
|
||||
description: "该操作不可恢复。",
|
||||
description: t("core.common.dialog.descriptions.cannot_be_recovered"),
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await apiClient.extension.theme.deletethemeHaloRunV1alpha1Theme({
|
||||
|
@ -85,7 +89,7 @@ const handleUninstall = async (theme: Theme, deleteExtensions?: boolean) => {
|
|||
}
|
||||
}
|
||||
|
||||
Toast.success("卸载成功");
|
||||
Toast.success(t("core.common.toast.uninstall_success"));
|
||||
} catch (e) {
|
||||
console.error("Failed to uninstall theme", e);
|
||||
} finally {
|
||||
|
@ -115,14 +119,18 @@ const handleUninstall = async (theme: Theme, deleteExtensions?: boolean) => {
|
|||
<div
|
||||
class="flex h-full items-center justify-center object-cover"
|
||||
>
|
||||
<span class="text-xs text-gray-400">加载中...</span>
|
||||
<span class="text-xs text-gray-400">
|
||||
{{ $t("core.common.status.loading") }}...
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #error>
|
||||
<div
|
||||
class="flex h-full items-center justify-center object-cover"
|
||||
>
|
||||
<span class="text-xs text-red-400">加载异常</span>
|
||||
<span class="text-xs text-red-400">
|
||||
{{ $t("core.common.status.loading_error") }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</LazyImage>
|
||||
|
@ -135,7 +143,9 @@ const handleUninstall = async (theme: Theme, deleteExtensions?: boolean) => {
|
|||
:description="theme.spec.version"
|
||||
>
|
||||
<template #extra>
|
||||
<VTag v-if="isActivated"> 当前启用 </VTag>
|
||||
<VTag v-if="isActivated">
|
||||
{{ $t("core.common.status.activated") }}
|
||||
</VTag>
|
||||
</template>
|
||||
</VEntityField>
|
||||
</template>
|
||||
|
@ -147,7 +157,11 @@ const handleUninstall = async (theme: Theme, deleteExtensions?: boolean) => {
|
|||
</VEntityField>
|
||||
<VEntityField v-if="theme.metadata.deletionTimestamp">
|
||||
<template #description>
|
||||
<VStatusDot v-tooltip="`删除中`" state="warning" animate />
|
||||
<VStatusDot
|
||||
v-tooltip="$t('core.common.status.deleting')"
|
||||
state="warning"
|
||||
animate
|
||||
/>
|
||||
</template>
|
||||
</VEntityField>
|
||||
<VEntityField>
|
||||
|
@ -185,16 +199,18 @@ const handleUninstall = async (theme: Theme, deleteExtensions?: boolean) => {
|
|||
type="secondary"
|
||||
@click="handleActiveTheme"
|
||||
>
|
||||
启用
|
||||
{{ $t("core.common.buttons.active") }}
|
||||
</VButton>
|
||||
<VButton v-close-popper block type="default" @click="emit('upgrade')">
|
||||
升级
|
||||
{{ $t("core.common.buttons.upgrade") }}
|
||||
</VButton>
|
||||
<VButton v-close-popper block type="default" @click="emit('preview')">
|
||||
预览
|
||||
{{ $t("core.common.buttons.preview") }}
|
||||
</VButton>
|
||||
<FloatingDropdown class="w-full" placement="right" :triggers="['click']">
|
||||
<VButton block type="danger"> 卸载 </VButton>
|
||||
<VButton block type="danger">
|
||||
{{ $t("core.common.buttons.uninstall") }}
|
||||
</VButton>
|
||||
<template #popper>
|
||||
<div class="w-52 p-2">
|
||||
<VSpace class="w-full" direction="column">
|
||||
|
@ -204,7 +220,7 @@ const handleUninstall = async (theme: Theme, deleteExtensions?: boolean) => {
|
|||
type="danger"
|
||||
@click="handleUninstall(theme)"
|
||||
>
|
||||
卸载
|
||||
{{ $t("core.common.buttons.uninstall") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-close-popper.all
|
||||
|
@ -212,7 +228,9 @@ const handleUninstall = async (theme: Theme, deleteExtensions?: boolean) => {
|
|||
type="danger"
|
||||
@click="handleUninstall(theme, true)"
|
||||
>
|
||||
卸载并删除配置
|
||||
{{
|
||||
$t("core.theme.operations.uninstall_and_delete_config.button")
|
||||
}}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</div>
|
||||
|
@ -224,7 +242,7 @@ const handleUninstall = async (theme: Theme, deleteExtensions?: boolean) => {
|
|||
type="danger"
|
||||
@click="handleResetSettingConfig"
|
||||
>
|
||||
重置
|
||||
{{ $t("core.common.buttons.reset") }}
|
||||
</VButton>
|
||||
</template>
|
||||
</VEntity>
|
||||
|
|
|
@ -43,14 +43,18 @@ const { isActivated, handleActiveTheme } = useThemeLifeCycle(theme);
|
|||
<div
|
||||
class="flex h-full items-center justify-center object-cover"
|
||||
>
|
||||
<span class="text-xs text-gray-400">加载中...</span>
|
||||
<span class="text-xs text-gray-400">
|
||||
{{ $t("core.common.status.loading") }}...
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #error>
|
||||
<div
|
||||
class="flex h-full items-center justify-center object-cover"
|
||||
>
|
||||
<span class="text-xs text-red-400">加载异常</span>
|
||||
<span class="text-xs text-red-400">
|
||||
{{ $t("core.common.status.loading_error") }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</LazyImage>
|
||||
|
@ -63,7 +67,9 @@ const { isActivated, handleActiveTheme } = useThemeLifeCycle(theme);
|
|||
:description="theme.spec.version"
|
||||
>
|
||||
<template #extra>
|
||||
<VTag v-if="isActivated"> 当前启用 </VTag>
|
||||
<VTag v-if="isActivated">
|
||||
{{ $t("core.common.status.activated") }}
|
||||
</VTag>
|
||||
</template>
|
||||
</VEntityField>
|
||||
</template>
|
||||
|
@ -76,7 +82,7 @@ const { isActivated, handleActiveTheme } = useThemeLifeCycle(theme);
|
|||
type="secondary"
|
||||
@click="handleActiveTheme"
|
||||
>
|
||||
启用
|
||||
{{ $t("core.common.buttons.active") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-close-popper
|
||||
|
@ -84,7 +90,7 @@ const { isActivated, handleActiveTheme } = useThemeLifeCycle(theme);
|
|||
type="default"
|
||||
@click="emit('open-settings')"
|
||||
>
|
||||
设置
|
||||
{{ $t("core.common.buttons.setting") }}
|
||||
</VButton>
|
||||
</template>
|
||||
</VEntity>
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
} from "@halo-dev/components";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed, markRaw, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -44,6 +45,8 @@ const emit = defineEmits<{
|
|||
(event: "close"): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
interface SettingTab {
|
||||
id: string;
|
||||
label: string;
|
||||
|
@ -112,7 +115,9 @@ const modalTitle = computed(() => {
|
|||
if (props.title) {
|
||||
return props.title;
|
||||
}
|
||||
return `预览主题:${selectedTheme.value?.spec.displayName}`;
|
||||
return t("core.theme.preview_model.title", {
|
||||
display_name: selectedTheme.value?.spec.displayName,
|
||||
});
|
||||
});
|
||||
|
||||
// theme settings
|
||||
|
@ -164,7 +169,7 @@ const handleSaveConfigMap = async () => {
|
|||
configMap: configMapToUpdate,
|
||||
});
|
||||
|
||||
Toast.success("保存成功");
|
||||
Toast.success(t("core.common.toast.save_success"));
|
||||
|
||||
await handleFetchSettings();
|
||||
configMap.value = newConfigMap;
|
||||
|
@ -254,23 +259,40 @@ const iframeClasses = computed(() => {
|
|||
</template>
|
||||
<template #actions>
|
||||
<span
|
||||
v-tooltip="{ content: '切换主题', delay: 300 }"
|
||||
v-tooltip="{
|
||||
content: $t('core.theme.empty.actions.switch'),
|
||||
delay: 300,
|
||||
}"
|
||||
:class="{ 'bg-gray-200': themesVisible }"
|
||||
@click="handleOpenThemes"
|
||||
>
|
||||
<IconPalette />
|
||||
</span>
|
||||
<span
|
||||
v-tooltip="{ content: '主题设置', delay: 300 }"
|
||||
v-tooltip="{
|
||||
content: $t('core.theme.preview_model.actions.setting'),
|
||||
delay: 300,
|
||||
}"
|
||||
:class="{ 'bg-gray-200': settingsVisible }"
|
||||
@click="handleOpenSettings(undefined)"
|
||||
>
|
||||
<IconSettings />
|
||||
</span>
|
||||
<span v-tooltip="{ content: '刷新', delay: 300 }" @click="handleRefresh">
|
||||
<span
|
||||
v-tooltip="{
|
||||
content: $t('core.common.buttons.refresh'),
|
||||
delay: 300,
|
||||
}"
|
||||
@click="handleRefresh"
|
||||
>
|
||||
<IconRefreshLine />
|
||||
</span>
|
||||
<span v-tooltip="{ content: '新窗口打开', delay: 300 }">
|
||||
<span
|
||||
v-tooltip="{
|
||||
content: $t('core.theme.preview_model.actions.open'),
|
||||
delay: 300,
|
||||
}"
|
||||
>
|
||||
<a :href="previewUrl" target="_blank">
|
||||
<IconLink />
|
||||
</a>
|
||||
|
@ -347,7 +369,7 @@ const iframeClasses = computed(() => {
|
|||
)
|
||||
"
|
||||
>
|
||||
保存
|
||||
{{ $t("core.common.buttons.save") }}
|
||||
</VButton>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -5,6 +5,7 @@ import { apiClient } from "@/utils/api-client";
|
|||
import { Dialog, Toast } from "@halo-dev/components";
|
||||
import { useThemeStore } from "@/stores/theme";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
interface useThemeLifeCycleReturn {
|
||||
loading: Ref<boolean>;
|
||||
|
@ -17,6 +18,8 @@ interface useThemeLifeCycleReturn {
|
|||
export function useThemeLifeCycle(
|
||||
theme: Ref<Theme | undefined>
|
||||
): useThemeLifeCycleReturn {
|
||||
const { t } = useI18n();
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
|
@ -41,8 +44,10 @@ export function useThemeLifeCycle(
|
|||
|
||||
const handleActiveTheme = async () => {
|
||||
Dialog.info({
|
||||
title: "是否确认启用当前主题",
|
||||
title: t("core.theme.operations.active.title"),
|
||||
description: theme.value?.spec.displayName,
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
if (!theme.value) return;
|
||||
|
@ -51,7 +56,7 @@ export function useThemeLifeCycle(
|
|||
name: theme.value?.metadata.name,
|
||||
});
|
||||
|
||||
Toast.success("启用成功");
|
||||
Toast.success(t("core.theme.operations.active.toast_success"));
|
||||
} catch (e) {
|
||||
console.error("Failed to active theme", e);
|
||||
} finally {
|
||||
|
@ -63,9 +68,11 @@ export function useThemeLifeCycle(
|
|||
|
||||
const handleResetSettingConfig = async () => {
|
||||
Dialog.warning({
|
||||
title: "确定要重置主题的所有配置吗?",
|
||||
description: "该操作会删除已保存的配置,重置为默认配置。",
|
||||
title: t("core.theme.operations.reset.title"),
|
||||
description: t("core.theme.operations.reset.description"),
|
||||
confirmType: "danger",
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
if (!theme?.value) {
|
||||
|
@ -76,7 +83,7 @@ export function useThemeLifeCycle(
|
|||
name: theme.value.metadata.name as string,
|
||||
});
|
||||
|
||||
Toast.success("重置配置成功");
|
||||
Toast.success(t("core.theme.operations.reset.toast_success"));
|
||||
} catch (e) {
|
||||
console.error("Failed to reset theme setting config", e);
|
||||
}
|
||||
|
@ -95,10 +102,12 @@ export function useThemeLifeCycle(
|
|||
|
||||
export function useThemeCustomTemplates(type: "post" | "page" | "category") {
|
||||
const themeStore = useThemeStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const templates = computed(() => {
|
||||
const defaultTemplate = [
|
||||
{
|
||||
label: "默认模板",
|
||||
label: t("core.theme.custom_templates.default"),
|
||||
value: "",
|
||||
},
|
||||
];
|
||||
|
|
|
@ -32,8 +32,10 @@ import { usePermission } from "@/utils/permission";
|
|||
import { useThemeStore } from "@/stores/theme";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
const { t } = useI18n();
|
||||
|
||||
interface ThemeTab {
|
||||
id: string;
|
||||
|
@ -47,7 +49,7 @@ interface ThemeTab {
|
|||
const initialTabs: ThemeTab[] = [
|
||||
{
|
||||
id: "detail",
|
||||
label: "详情",
|
||||
label: t("core.theme.tabs.detail"),
|
||||
route: {
|
||||
name: "ThemeDetail",
|
||||
},
|
||||
|
@ -179,19 +181,19 @@ onMounted(() => {
|
|||
type="primary"
|
||||
@click="handleActiveTheme"
|
||||
>
|
||||
启用
|
||||
{{ $t("core.common.buttons.active") }}
|
||||
</VButton>
|
||||
<VButton type="secondary" size="sm" @click="previewModal = true">
|
||||
<template #icon>
|
||||
<IconEye class="h-full w-full" />
|
||||
</template>
|
||||
预览
|
||||
{{ $t("core.common.buttons.preview") }}
|
||||
</VButton>
|
||||
<VButton size="sm" type="default" @click="themesModal = true">
|
||||
<template #icon>
|
||||
<IconExchange class="h-full w-full" />
|
||||
</template>
|
||||
主题管理
|
||||
{{ $t("core.theme.actions.management") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
|
@ -200,17 +202,19 @@ onMounted(() => {
|
|||
<div class="m-0 md:m-4">
|
||||
<VEmpty
|
||||
v-if="!selectedTheme && !loading"
|
||||
message="当前没有已激活或者选择的主题,你可以切换主题或者安装新主题"
|
||||
title="当前没有已激活或已选择的主题"
|
||||
:message="$t('core.theme.empty.message')"
|
||||
:title="$t('core.theme.empty.title')"
|
||||
>
|
||||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton @click="themesModal = true"> 安装主题 </VButton>
|
||||
<VButton @click="themesModal = true">
|
||||
{{ $t("core.theme.common.buttons.install") }}
|
||||
</VButton>
|
||||
<VButton type="primary" @click="themesModal = true">
|
||||
<template #icon>
|
||||
<IconExchange class="h-full w-full" />
|
||||
</template>
|
||||
切换主题
|
||||
{{ $t("core.theme.empty.actions.switch") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
|
|
|
@ -17,11 +17,11 @@ export default definePlugin({
|
|||
name: "ThemeDetail",
|
||||
component: ThemeDetail,
|
||||
meta: {
|
||||
title: "主题",
|
||||
title: "core.theme.title",
|
||||
searchable: true,
|
||||
permissions: ["system:themes:view"],
|
||||
menu: {
|
||||
name: "主题",
|
||||
name: "core.sidebar.menu.items.themes",
|
||||
group: "interface",
|
||||
icon: markRaw(IconPalette),
|
||||
priority: 0,
|
||||
|
@ -33,7 +33,7 @@ export default definePlugin({
|
|||
name: "ThemeSetting",
|
||||
component: ThemeSetting,
|
||||
meta: {
|
||||
title: "主题设置",
|
||||
title: "core.theme.settings.title",
|
||||
permissions: ["system:themes:view"],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -13,6 +13,9 @@ import type { Info, GlobalInfo, Startup } from "./types";
|
|||
import axios from "axios";
|
||||
import { formatDatetime } from "@/utils/date";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const info = ref<Info>();
|
||||
const globalInfo = ref<GlobalInfo>();
|
||||
|
@ -68,23 +71,36 @@ const { copy, isSupported } = useClipboard();
|
|||
|
||||
const handleCopy = () => {
|
||||
if (!isSupported.value) {
|
||||
Toast.warning("当前浏览器不支持复制");
|
||||
Toast.warning(t("core.actuator.actions.copy.toast_browser_not_supported"));
|
||||
return;
|
||||
}
|
||||
|
||||
const text = `
|
||||
- 外部访问地址:${globalInfo.value?.externalUrl}
|
||||
- 启动时间:${formatDatetime(startup.value?.timeline.startTime)}
|
||||
- Halo 版本:${info.value?.build?.version}
|
||||
- 构建时间:${formatDatetime(info.value?.build?.time)}
|
||||
- ${t("core.actuator.copy_results.external_url", {
|
||||
external_url: globalInfo.value?.externalUrl,
|
||||
})}
|
||||
- ${t("core.actuator.copy_results.start_time", {
|
||||
start_time: formatDatetime(startup.value?.timeline.startTime),
|
||||
})}
|
||||
- ${t("core.actuator.fields.version", { version: info.value?.build?.version })}
|
||||
- ${t("core.actuator.copy_results.build_time", {
|
||||
build_time: formatDatetime(info.value?.build?.time),
|
||||
})}
|
||||
- Git Commit:${info.value?.git?.commit.id}
|
||||
- Java:${info.value?.java.runtime.name} / ${info.value?.java.runtime.version}
|
||||
- 数据库:${info.value?.database.name} / ${info.value?.database.version}
|
||||
- 操作系统:${info.value?.os.name} / ${info.value?.os.version}
|
||||
- ${t("core.actuator.copy_results.database", {
|
||||
database: [info.value?.database.name, info.value?.database.version].join(
|
||||
" / "
|
||||
),
|
||||
})}
|
||||
- ${t("core.actuator.copy_results.os", {
|
||||
os: [info.value?.os.name, info.value?.os.version].join(" / "),
|
||||
})}
|
||||
`;
|
||||
|
||||
copy(text);
|
||||
|
||||
Toast.success("复制成功");
|
||||
Toast.success(t("core.common.toast.copy_success"));
|
||||
};
|
||||
|
||||
const handleDownloadLogfile = () => {
|
||||
|
@ -101,17 +117,17 @@ const handleDownloadLogfile = () => {
|
|||
document.body.removeChild(downloadElement);
|
||||
window.URL.revokeObjectURL(href);
|
||||
|
||||
Toast.success("下载成功");
|
||||
Toast.success(t("core.common.toast.download_success"));
|
||||
})
|
||||
.catch((e) => {
|
||||
Toast.error("下载失败");
|
||||
Toast.error(t("core.common.toast.download_failed"));
|
||||
console.log("Failed to download log file.", e);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPageHeader title="系统概览">
|
||||
<VPageHeader :title="$t('core.actuator.title')">
|
||||
<template #icon>
|
||||
<IconTerminalBoxLine class="mr-2 self-center" />
|
||||
</template>
|
||||
|
@ -120,7 +136,7 @@ const handleDownloadLogfile = () => {
|
|||
<template #icon>
|
||||
<IconClipboardLine class="h-full w-full" />
|
||||
</template>
|
||||
复制
|
||||
{{ $t("core.common.buttons.copy") }}
|
||||
</VButton>
|
||||
</template>
|
||||
</VPageHeader>
|
||||
|
@ -133,7 +149,7 @@ const handleDownloadLogfile = () => {
|
|||
>
|
||||
<div>
|
||||
<h3 class="text-lg font-medium leading-6 text-gray-900">
|
||||
基本信息
|
||||
{{ $t("core.actuator.header.titles.general") }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -142,7 +158,9 @@ const handleDownloadLogfile = () => {
|
|||
<div
|
||||
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
|
||||
>
|
||||
<dt class="text-sm font-medium text-gray-900">外部访问地址</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.actuator.fields.external_url") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||
<span>
|
||||
{{ globalInfo?.externalUrl }}
|
||||
|
@ -151,11 +169,11 @@ const handleDownloadLogfile = () => {
|
|||
v-if="!isExternalUrlValid"
|
||||
class="mt-3"
|
||||
type="warning"
|
||||
title="警告"
|
||||
:title="$t('core.common.text.warning')"
|
||||
:closable="false"
|
||||
>
|
||||
<template #description>
|
||||
检测到外部访问地址与当前访问地址不一致,可能会导致部分链接无法正常跳转,请检查外部访问地址设置。
|
||||
{{ $t("core.actuator.alert.external_url_invalid") }}
|
||||
</template>
|
||||
</VAlert>
|
||||
</dd>
|
||||
|
@ -163,7 +181,9 @@ const handleDownloadLogfile = () => {
|
|||
<div
|
||||
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
|
||||
>
|
||||
<dt class="text-sm font-medium text-gray-900">启动时间</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.actuator.fields.start_time") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||
{{ formatDatetime(startup?.timeline.startTime) }}
|
||||
</dd>
|
||||
|
@ -171,7 +191,9 @@ const handleDownloadLogfile = () => {
|
|||
<div
|
||||
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
|
||||
>
|
||||
<dt class="text-sm font-medium text-gray-900">时区</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.actuator.fields.timezone") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||
{{ globalInfo?.timeZone }}
|
||||
</dd>
|
||||
|
@ -179,7 +201,9 @@ const handleDownloadLogfile = () => {
|
|||
<div
|
||||
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
|
||||
>
|
||||
<dt class="text-sm font-medium text-gray-900">语言</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.actuator.fields.locale") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||
{{ globalInfo?.locale }}
|
||||
</dd>
|
||||
|
@ -195,7 +219,7 @@ const handleDownloadLogfile = () => {
|
|||
>
|
||||
<div>
|
||||
<h3 class="text-lg font-medium leading-6 text-gray-900">
|
||||
环境信息
|
||||
{{ $t("core.actuator.header.titles.environment") }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -205,7 +229,9 @@ const handleDownloadLogfile = () => {
|
|||
v-if="info.build"
|
||||
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
|
||||
>
|
||||
<dt class="text-sm font-medium text-gray-900">版本</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.actuator.fields.version") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||
<a
|
||||
:href="`https://github.com/halo-dev/halo/releases/tag/v${info.build.version}`"
|
||||
|
@ -220,7 +246,9 @@ const handleDownloadLogfile = () => {
|
|||
v-if="info.build"
|
||||
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
|
||||
>
|
||||
<dt class="text-sm font-medium text-gray-900">构建时间</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.actuator.fields.build_time") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||
{{ formatDatetime(info.build.time) }}
|
||||
</dd>
|
||||
|
@ -251,7 +279,9 @@ const handleDownloadLogfile = () => {
|
|||
<div
|
||||
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
|
||||
>
|
||||
<dt class="text-sm font-medium text-gray-900">数据库</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.actuator.fields.database") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||
{{ [info.database.name, info.database.version].join(" / ") }}
|
||||
</dd>
|
||||
|
@ -259,7 +289,9 @@ const handleDownloadLogfile = () => {
|
|||
<div
|
||||
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
|
||||
>
|
||||
<dt class="text-sm font-medium text-gray-900">操作系统</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.actuator.fields.os") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||
{{ info.os.name }} {{ info.os.version }} / {{ info.os.arch }}
|
||||
</dd>
|
||||
|
@ -267,10 +299,12 @@ const handleDownloadLogfile = () => {
|
|||
<div
|
||||
class="items-center bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
|
||||
>
|
||||
<dt class="text-sm font-medium text-gray-900">运行日志</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.actuator.fields.log") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||
<VButton size="sm" @click="handleDownloadLogfile()">
|
||||
下载
|
||||
{{ $t("core.common.buttons.download") }}
|
||||
</VButton>
|
||||
</dd>
|
||||
</div>
|
||||
|
|
|
@ -15,10 +15,10 @@ export default definePlugin({
|
|||
path: "",
|
||||
component: Actuator,
|
||||
meta: {
|
||||
title: "系统概览",
|
||||
title: "core.actuator.title",
|
||||
searchable: true,
|
||||
menu: {
|
||||
name: "概览",
|
||||
name: "core.sidebar.menu.items.actuator",
|
||||
group: "system",
|
||||
icon: markRaw(IconTerminalBoxLine),
|
||||
priority: 3,
|
||||
|
|
|
@ -13,13 +13,15 @@ import {
|
|||
VTabbar,
|
||||
} from "@halo-dev/components";
|
||||
import { useSettingFormConvert } from "@/composables/use-setting-form";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
|
||||
const tabs = ref<{ id: string; label: string }[]>([
|
||||
{
|
||||
id: "detail",
|
||||
label: "详情",
|
||||
label: t("core.identity_authentication.tabs.detail"),
|
||||
},
|
||||
]);
|
||||
|
||||
|
@ -40,7 +42,7 @@ const { data: authProvider } = useQuery<AuthProvider>({
|
|||
if (data.spec.settingRef?.name) {
|
||||
tabs.value.push({
|
||||
id: "setting",
|
||||
label: "设置",
|
||||
label: t("core.identity_authentication.tabs.setting"),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -132,7 +134,7 @@ const handleSaveConfigMap = async () => {
|
|||
configMap: configMapToUpdate,
|
||||
});
|
||||
|
||||
Toast.success("保存成功");
|
||||
Toast.success(t("core.common.toast.save_success"));
|
||||
|
||||
await handleFetchSettings();
|
||||
await handleFetchConfigMap();
|
||||
|
@ -169,7 +171,11 @@ const handleSaveConfigMap = async () => {
|
|||
<div
|
||||
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
|
||||
>
|
||||
<dt class="text-sm font-medium text-gray-900">名称</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{
|
||||
$t("core.identity_authentication.detail.fields.display_name")
|
||||
}}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
|
||||
{{ authProvider?.spec.displayName }}
|
||||
</dd>
|
||||
|
@ -177,7 +183,11 @@ const handleSaveConfigMap = async () => {
|
|||
<div
|
||||
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
|
||||
>
|
||||
<dt class="text-sm font-medium text-gray-900">描述</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{
|
||||
$t("core.identity_authentication.detail.fields.description")
|
||||
}}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
|
||||
{{ authProvider?.spec.description }}
|
||||
</dd>
|
||||
|
@ -185,7 +195,9 @@ const handleSaveConfigMap = async () => {
|
|||
<div
|
||||
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
|
||||
>
|
||||
<dt class="text-sm font-medium text-gray-900">网站</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.identity_authentication.detail.fields.website") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
|
||||
<a
|
||||
v-if="authProvider?.spec.website"
|
||||
|
@ -194,13 +206,17 @@ const handleSaveConfigMap = async () => {
|
|||
>
|
||||
{{ authProvider.spec.website }}
|
||||
</a>
|
||||
<span v-else>无</span>
|
||||
<span v-else>
|
||||
{{ $t("core.common.text.none") }}
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div
|
||||
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
|
||||
>
|
||||
<dt class="text-sm font-medium text-gray-900">帮助页面</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.identity_authentication.detail.fields.help_page") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
|
||||
<a
|
||||
v-if="authProvider?.spec.helpPage"
|
||||
|
@ -209,13 +225,19 @@ const handleSaveConfigMap = async () => {
|
|||
>
|
||||
{{ authProvider.spec.helpPage }}
|
||||
</a>
|
||||
<span v-else>无</span>
|
||||
<span v-else>{{ $t("core.common.text.none") }}</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div
|
||||
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
|
||||
>
|
||||
<dt class="text-sm font-medium text-gray-900">登录入口</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{
|
||||
$t(
|
||||
"core.identity_authentication.detail.fields.authentication_url"
|
||||
)
|
||||
}}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-3 sm:mt-0">
|
||||
{{ authProvider?.spec.authenticationUrl }}
|
||||
</dd>
|
||||
|
@ -247,7 +269,7 @@ const handleSaveConfigMap = async () => {
|
|||
type="secondary"
|
||||
@click="$formkit.submit(group || '')"
|
||||
>
|
||||
保存
|
||||
{{ $t("core.common.buttons.save") }}
|
||||
</VButton>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -20,7 +20,7 @@ const {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<VPageHeader title="身份认证">
|
||||
<VPageHeader :title="$t('core.identity_authentication.title')">
|
||||
<template #icon>
|
||||
<IconLockPasswordLine class="mr-2 self-center" />
|
||||
</template>
|
||||
|
@ -34,7 +34,10 @@ const {
|
|||
class="relative flex flex-col items-start sm:flex-row sm:items-center"
|
||||
>
|
||||
<div class="flex w-full flex-1 sm:w-auto">
|
||||
<FormKit placeholder="输入关键词搜索" type="text"></FormKit>
|
||||
<FormKit
|
||||
:placeholder="$t('core.common.placeholder.search')"
|
||||
type="text"
|
||||
></FormKit>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -9,6 +9,9 @@ import {
|
|||
VEntityField,
|
||||
VSwitch,
|
||||
} from "@halo-dev/components";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
authProvider: ListedAuthProvider;
|
||||
|
@ -20,9 +23,11 @@ const emit = defineEmits<{
|
|||
|
||||
const handleChangeStatus = async () => {
|
||||
Dialog.info({
|
||||
title: `确定要${
|
||||
props.authProvider.enabled ? "停用" : "启用"
|
||||
}该身份认证方式吗?`,
|
||||
title: props.authProvider.enabled
|
||||
? t("core.identity_authentication.operations.disable.title")
|
||||
: t("core.identity_authentication.operations.enable.title"),
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
if (props.authProvider.enabled) {
|
||||
|
@ -30,12 +35,12 @@ const handleChangeStatus = async () => {
|
|||
name: props.authProvider.name,
|
||||
});
|
||||
|
||||
Toast.success("停用成功");
|
||||
Toast.success(t("core.common.toast.inactive_success"));
|
||||
} else {
|
||||
await apiClient.authProvider.enableAuthProvider({
|
||||
name: props.authProvider.name,
|
||||
});
|
||||
Toast.success("启用成功");
|
||||
Toast.success(t("core.common.toast.active_success"));
|
||||
}
|
||||
|
||||
emit("reload");
|
||||
|
|
|
@ -56,13 +56,19 @@ const pluginRoleTemplateGroups = computed<RoleTemplateGroup[]>(() => {
|
|||
<div>
|
||||
<div class="flex items-center justify-between bg-white px-4 py-4 sm:px-6">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium leading-6 text-gray-900">插件信息</h3>
|
||||
<h3 class="text-lg font-medium leading-6 text-gray-900">
|
||||
{{ $t("core.plugin.detail.header.title") }}
|
||||
</h3>
|
||||
<p class="mt-1 flex max-w-2xl items-center gap-2">
|
||||
<span class="text-sm text-gray-500">{{
|
||||
plugin?.spec.version
|
||||
}}</span>
|
||||
<span class="text-sm text-gray-500">
|
||||
{{ plugin?.spec.version }}
|
||||
</span>
|
||||
<VTag>
|
||||
{{ isStarted ? "已启用" : "未启用" }}
|
||||
{{
|
||||
isStarted
|
||||
? $t("core.common.status.activated")
|
||||
: $t("core.common.status.not_activated")
|
||||
}}
|
||||
</VTag>
|
||||
</p>
|
||||
</div>
|
||||
|
@ -75,7 +81,9 @@ const pluginRoleTemplateGroups = computed<RoleTemplateGroup[]>(() => {
|
|||
<div
|
||||
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
|
||||
>
|
||||
<dt class="text-sm font-medium text-gray-900">名称</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.plugin.detail.fields.display_name") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||
{{ plugin?.spec.displayName }}
|
||||
</dd>
|
||||
|
@ -83,7 +91,9 @@ const pluginRoleTemplateGroups = computed<RoleTemplateGroup[]>(() => {
|
|||
<div
|
||||
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
|
||||
>
|
||||
<dt class="text-sm font-medium text-gray-900">描述</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.plugin.detail.fields.description") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||
{{ plugin?.spec.description }}
|
||||
</dd>
|
||||
|
@ -91,7 +101,9 @@ const pluginRoleTemplateGroups = computed<RoleTemplateGroup[]>(() => {
|
|||
<div
|
||||
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
|
||||
>
|
||||
<dt class="text-sm font-medium text-gray-900">版本</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.plugin.detail.fields.version") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||
{{ plugin?.spec.version }}
|
||||
</dd>
|
||||
|
@ -99,7 +111,9 @@ const pluginRoleTemplateGroups = computed<RoleTemplateGroup[]>(() => {
|
|||
<div
|
||||
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
|
||||
>
|
||||
<dt class="text-sm font-medium text-gray-900">Halo 版本要求</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.plugin.detail.fields.requires") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||
{{ plugin?.spec.requires }}
|
||||
</dd>
|
||||
|
@ -107,7 +121,9 @@ const pluginRoleTemplateGroups = computed<RoleTemplateGroup[]>(() => {
|
|||
<div
|
||||
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
|
||||
>
|
||||
<dt class="text-sm font-medium text-gray-900">提供方</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.plugin.detail.fields.author") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||
<a
|
||||
v-if="plugin?.spec.author"
|
||||
|
@ -116,13 +132,17 @@ const pluginRoleTemplateGroups = computed<RoleTemplateGroup[]>(() => {
|
|||
>
|
||||
{{ plugin?.spec.author.name }}
|
||||
</a>
|
||||
<span v-else>无</span>
|
||||
<span v-else>
|
||||
{{ $t("core.common.text.none") }}
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div
|
||||
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
|
||||
>
|
||||
<dt class="text-sm font-medium text-gray-900">协议</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.plugin.detail.fields.license") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||
<ul
|
||||
v-if="plugin?.spec.license && plugin?.spec.license.length"
|
||||
|
@ -150,7 +170,9 @@ const pluginRoleTemplateGroups = computed<RoleTemplateGroup[]>(() => {
|
|||
>
|
||||
<dt class="text-sm font-medium text-gray-900">模型定义</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||
<span>无</span>
|
||||
<span>
|
||||
{{ $t("core.common.text.none") }}
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div
|
||||
|
@ -159,7 +181,9 @@ const pluginRoleTemplateGroups = computed<RoleTemplateGroup[]>(() => {
|
|||
}`"
|
||||
class="px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
|
||||
>
|
||||
<dt class="text-sm font-medium text-gray-900">权限模板</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.plugin.detail.fields.role_templates") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-5 sm:mt-0">
|
||||
<dl
|
||||
v-if="pluginRoleTemplateGroups.length"
|
||||
|
@ -195,13 +219,14 @@ const pluginRoleTemplateGroups = computed<RoleTemplateGroup[]>(() => {
|
|||
"
|
||||
class="text-xs text-gray-400"
|
||||
>
|
||||
依赖于
|
||||
{{
|
||||
JSON.parse(
|
||||
role.metadata.annotations?.[
|
||||
rbacAnnotations.DEPENDENCIES
|
||||
]
|
||||
).join(", ")
|
||||
$t("core.role.common.text.dependent_on", {
|
||||
roles: JSON.parse(
|
||||
role.metadata.annotations?.[
|
||||
rbacAnnotations.DEPENDENCIES
|
||||
]
|
||||
).join(", "),
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
|
@ -211,13 +236,17 @@ const pluginRoleTemplateGroups = computed<RoleTemplateGroup[]>(() => {
|
|||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<span v-else>无</span>
|
||||
<span v-else>
|
||||
{{ $t("core.common.text.none") }}
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div
|
||||
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-6 sm:gap-4 sm:px-6"
|
||||
>
|
||||
<dt class="text-sm font-medium text-gray-900">最近一次启动</dt>
|
||||
<dt class="text-sm font-medium text-gray-900">
|
||||
{{ $t("core.plugin.detail.fields.last_starttime") }}
|
||||
</dt>
|
||||
<dd
|
||||
class="mt-1 text-sm tabular-nums text-gray-900 sm:col-span-2 sm:mt-0"
|
||||
>
|
||||
|
|
|
@ -18,10 +18,13 @@ import { computed, ref } from "vue";
|
|||
import { apiClient } from "@/utils/api-client";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
import FilterTag from "@/components/filter/FilterTag.vue";
|
||||
import FilteCleanButton from "@/components/filter/FilterCleanButton.vue";
|
||||
import FilterCleanButton from "@/components/filter/FilterCleanButton.vue";
|
||||
import { getNode } from "@formkit/core";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import type { Plugin } from "@halo-dev/api-client";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
interface EnabledItem {
|
||||
label: string;
|
||||
|
@ -45,26 +48,26 @@ const total = ref(0);
|
|||
// Filters
|
||||
const EnabledItems: EnabledItem[] = [
|
||||
{
|
||||
label: "全部",
|
||||
label: t("core.plugin.filters.status.items.all"),
|
||||
value: undefined,
|
||||
},
|
||||
{
|
||||
label: "已启用",
|
||||
label: t("core.plugin.filters.status.items.active"),
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
label: "未启用",
|
||||
label: t("core.plugin.filters.status.items.inactive"),
|
||||
value: false,
|
||||
},
|
||||
];
|
||||
|
||||
const SortItems: SortItem[] = [
|
||||
{
|
||||
label: "较近安装",
|
||||
label: t("core.plugin.filters.sort.items.create_time_desc"),
|
||||
value: "creationTimestamp,desc",
|
||||
},
|
||||
{
|
||||
label: "较早安装",
|
||||
label: t("core.plugin.filters.sort.items.create_time_asc"),
|
||||
value: "creationTimestamp,asc",
|
||||
},
|
||||
];
|
||||
|
@ -150,7 +153,7 @@ const { data, isLoading, isFetching, refetch } = useQuery<Plugin[]>({
|
|||
@close="refetch()"
|
||||
/>
|
||||
|
||||
<VPageHeader title="插件">
|
||||
<VPageHeader :title="$t('core.plugin.title')">
|
||||
<template #icon>
|
||||
<IconPlug class="mr-2 self-center" />
|
||||
</template>
|
||||
|
@ -163,7 +166,7 @@ const { data, isLoading, isFetching, refetch } = useQuery<Plugin[]>({
|
|||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
安装
|
||||
{{ $t("core.common.buttons.install") }}
|
||||
</VButton>
|
||||
</template>
|
||||
</VPageHeader>
|
||||
|
@ -179,7 +182,7 @@ const { data, isLoading, isFetching, refetch } = useQuery<Plugin[]>({
|
|||
<FormKit
|
||||
id="keywordInput"
|
||||
outer-class="!p-0"
|
||||
placeholder="输入关键词搜索"
|
||||
:placeholder="$t('core.common.placeholder.search')"
|
||||
type="text"
|
||||
name="keyword"
|
||||
:model-value="keyword"
|
||||
|
@ -187,24 +190,39 @@ const { data, isLoading, isFetching, refetch } = useQuery<Plugin[]>({
|
|||
></FormKit>
|
||||
|
||||
<FilterTag v-if="keyword" @close="handleClearKeyword()">
|
||||
关键词:{{ keyword }}
|
||||
{{
|
||||
$t("core.common.filters.results.keyword", {
|
||||
keyword: keyword,
|
||||
})
|
||||
}}
|
||||
</FilterTag>
|
||||
|
||||
<FilterTag
|
||||
v-if="selectedEnabledItem?.value !== undefined"
|
||||
@close="handleEnabledItemChange(EnabledItems[0])"
|
||||
>
|
||||
启用状态:{{ selectedEnabledItem.label }}
|
||||
{{
|
||||
$t("core.common.filters.results.status", {
|
||||
status: selectedEnabledItem.label,
|
||||
})
|
||||
}}
|
||||
</FilterTag>
|
||||
|
||||
<FilterTag
|
||||
v-if="selectedSortItem"
|
||||
@close="handleSortItemChange()"
|
||||
>
|
||||
排序:{{ selectedSortItem.label }}
|
||||
{{
|
||||
$t("core.common.filters.results.sort", {
|
||||
sort: selectedSortItem.label,
|
||||
})
|
||||
}}
|
||||
</FilterTag>
|
||||
|
||||
<FilteCleanButton v-if="hasFilters" @click="handleClearFilters" />
|
||||
<FilterCleanButton
|
||||
v-if="hasFilters"
|
||||
@click="handleClearFilters"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 flex sm:mt-0">
|
||||
<VSpace spacing="lg">
|
||||
|
@ -212,7 +230,9 @@ const { data, isLoading, isFetching, refetch } = useQuery<Plugin[]>({
|
|||
<div
|
||||
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
||||
>
|
||||
<span class="mr-0.5">启用状态</span>
|
||||
<span class="mr-0.5">
|
||||
{{ $t("core.common.filters.labels.status") }}
|
||||
</span>
|
||||
<span>
|
||||
<IconArrowDown />
|
||||
</span>
|
||||
|
@ -241,7 +261,9 @@ const { data, isLoading, isFetching, refetch } = useQuery<Plugin[]>({
|
|||
<div
|
||||
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
||||
>
|
||||
<span class="mr-0.5">排序</span>
|
||||
<span class="mr-0.5">
|
||||
{{ $t("core.common.filters.labels.sort") }}
|
||||
</span>
|
||||
<span>
|
||||
<IconArrowDown />
|
||||
</span>
|
||||
|
@ -268,7 +290,7 @@ const { data, isLoading, isFetching, refetch } = useQuery<Plugin[]>({
|
|||
@click="refetch()"
|
||||
>
|
||||
<IconRefreshLine
|
||||
v-tooltip="`刷新`"
|
||||
v-tooltip="$t('core.common.buttons.refresh')"
|
||||
:class="{ 'animate-spin text-gray-900': isFetching }"
|
||||
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
|
||||
/>
|
||||
|
@ -284,12 +306,14 @@ const { data, isLoading, isFetching, refetch } = useQuery<Plugin[]>({
|
|||
|
||||
<Transition v-else-if="!data?.length" appear name="fade">
|
||||
<VEmpty
|
||||
message="当前没有已安装的插件,你可以尝试刷新或者安装新插件"
|
||||
title="当前没有已安装的插件"
|
||||
:message="$t('core.plugin.empty.message')"
|
||||
:title="$t('core.plugin.empty.title')"
|
||||
>
|
||||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton :loading="isFetching" @click="refetch()">刷新</VButton>
|
||||
<VButton :loading="isFetching" @click="refetch()">
|
||||
{{ $t("core.common.buttons.refresh") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-permission="['system:plugins:manage']"
|
||||
type="secondary"
|
||||
|
@ -298,7 +322,7 @@ const { data, isLoading, isFetching, refetch } = useQuery<Plugin[]>({
|
|||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
安装插件
|
||||
{{ $t("core.plugin.empty.actions.install") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
|
@ -321,6 +345,8 @@ const { data, isLoading, isFetching, refetch } = useQuery<Plugin[]>({
|
|||
<VPagination
|
||||
v-model:page="page"
|
||||
v-model:size="size"
|
||||
:page-label="$t('core.components.pagination.page_label')"
|
||||
:size-label="$t('core.components.pagination.size_label')"
|
||||
:total="total"
|
||||
:size-options="[10, 20, 30, 50, 100]"
|
||||
/>
|
||||
|
|
|
@ -12,6 +12,9 @@ import { Toast, VButton } from "@halo-dev/components";
|
|||
// types
|
||||
import type { ConfigMap, Plugin, Setting } from "@halo-dev/api-client";
|
||||
import { useRouteParams } from "@vueuse/router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const group = useRouteParams<string>("group");
|
||||
|
||||
|
@ -55,7 +58,7 @@ const handleSaveConfigMap = async () => {
|
|||
configMap: configMapToUpdate,
|
||||
});
|
||||
|
||||
Toast.success("保存成功");
|
||||
Toast.success(t("core.common.toast.save_success"));
|
||||
|
||||
await handleFetchSettings();
|
||||
configMap.value = newConfigMap;
|
||||
|
@ -92,7 +95,7 @@ await handleFetchConfigMap();
|
|||
type="secondary"
|
||||
@click="$formkit.submit(group || '')"
|
||||
>
|
||||
保存
|
||||
{{ $t("core.common.buttons.save") }}
|
||||
</VButton>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -18,8 +18,10 @@ import type { Plugin } from "@halo-dev/api-client";
|
|||
import { formatDatetime } from "@/utils/date";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -46,9 +48,11 @@ const onUpgradeModalClose = () => {
|
|||
|
||||
const handleResetSettingConfig = async () => {
|
||||
Dialog.warning({
|
||||
title: "确定要重置插件的所有配置吗?",
|
||||
description: "该操作会删除已保存的配置,重置为默认配置。",
|
||||
title: t("core.plugin.operations.reset.title"),
|
||||
description: t("core.plugin.operations.reset.description"),
|
||||
confirmType: "danger",
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
if (!plugin?.value) {
|
||||
|
@ -59,7 +63,7 @@ const handleResetSettingConfig = async () => {
|
|||
name: plugin.value.metadata.name as string,
|
||||
});
|
||||
|
||||
Toast.success("重置配置成功");
|
||||
Toast.success(t("core.plugin.operations.reset.toast_success"));
|
||||
} catch (e) {
|
||||
console.error("Failed to reset plugin setting config", e);
|
||||
}
|
||||
|
@ -102,7 +106,11 @@ const getFailedMessage = (plugin: Plugin) => {
|
|||
<template #extra>
|
||||
<VSpace>
|
||||
<VTag>
|
||||
{{ isStarted ? "已启用" : "未启用" }}
|
||||
{{
|
||||
isStarted
|
||||
? $t("core.common.status.activated")
|
||||
: $t("core.common.status.not_activated")
|
||||
}}
|
||||
</VTag>
|
||||
</VSpace>
|
||||
</template>
|
||||
|
@ -120,7 +128,11 @@ const getFailedMessage = (plugin: Plugin) => {
|
|||
</VEntityField>
|
||||
<VEntityField v-if="plugin?.metadata.deletionTimestamp">
|
||||
<template #description>
|
||||
<VStatusDot v-tooltip="`删除中`" state="warning" animate />
|
||||
<VStatusDot
|
||||
v-tooltip="$t('core.common.status.deleting')"
|
||||
state="warning"
|
||||
animate
|
||||
/>
|
||||
</template>
|
||||
</VEntityField>
|
||||
<VEntityField v-if="plugin?.spec.author">
|
||||
|
@ -163,10 +175,12 @@ const getFailedMessage = (plugin: Plugin) => {
|
|||
type="secondary"
|
||||
@click="upgradeModal = true"
|
||||
>
|
||||
升级
|
||||
{{ $t("core.common.buttons.upgrade") }}
|
||||
</VButton>
|
||||
<FloatingDropdown class="w-full" placement="left" :triggers="['click']">
|
||||
<VButton block type="danger"> 卸载 </VButton>
|
||||
<VButton block type="danger">
|
||||
{{ $t("core.common.buttons.uninstall") }}
|
||||
</VButton>
|
||||
<template #popper>
|
||||
<div class="w-52 p-2">
|
||||
<VSpace class="w-full" direction="column">
|
||||
|
@ -176,7 +190,7 @@ const getFailedMessage = (plugin: Plugin) => {
|
|||
type="danger"
|
||||
@click="uninstall"
|
||||
>
|
||||
卸载
|
||||
{{ $t("core.common.buttons.uninstall") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-close-popper.all
|
||||
|
@ -184,7 +198,7 @@ const getFailedMessage = (plugin: Plugin) => {
|
|||
type="danger"
|
||||
@click="uninstall(true)"
|
||||
>
|
||||
卸载并删除配置
|
||||
{{ $t("core.plugin.list.actions.uninstall_and_delete_config") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</div>
|
||||
|
@ -196,7 +210,7 @@ const getFailedMessage = (plugin: Plugin) => {
|
|||
type="danger"
|
||||
@click="handleResetSettingConfig"
|
||||
>
|
||||
重置
|
||||
{{ $t("core.common.buttons.reset") }}
|
||||
</VButton>
|
||||
</template>
|
||||
</VEntity>
|
||||
|
|
|
@ -6,6 +6,9 @@ import type { Plugin } from "@halo-dev/api-client";
|
|||
import { computed, ref, watch } from "vue";
|
||||
import type { SuccessResponse, ErrorResponse } from "@uppy/core";
|
||||
import type { UppyFile } from "@uppy/utils";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -27,8 +30,10 @@ const uploadVisible = ref(false);
|
|||
|
||||
const modalTitle = computed(() => {
|
||||
return props.upgradePlugin
|
||||
? `升级插件(${props.upgradePlugin.spec.displayName})`
|
||||
: "安装插件";
|
||||
? t("core.plugin.upload_modal.titles.upgrade", {
|
||||
display_name: props.upgradePlugin.spec.displayName,
|
||||
})
|
||||
: t("core.plugin.upload_modal.titles.install");
|
||||
});
|
||||
|
||||
const handleVisibleChange = (visible: boolean) => {
|
||||
|
@ -54,8 +59,12 @@ const onUploaded = async (response: SuccessResponse) => {
|
|||
const plugin = response.body as Plugin;
|
||||
handleVisibleChange(false);
|
||||
Dialog.success({
|
||||
title: "上传成功",
|
||||
description: "是否启动当前安装的插件?",
|
||||
title: t("core.plugin.upload_modal.operations.active_after_install.title"),
|
||||
description: t(
|
||||
"core.plugin.upload_modal.operations.active_after_install.description"
|
||||
),
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
const { data: pluginToUpdate } =
|
||||
|
@ -95,15 +104,21 @@ const onError = (file: UppyFile<unknown>, response: ErrorResponse) => {
|
|||
const body = response.body as PluginInstallationErrorResponse;
|
||||
if (body.type === PLUGIN_ALREADY_EXISTS_TYPE) {
|
||||
Dialog.info({
|
||||
title: "插件已存在",
|
||||
description: "当前安装的插件已存在,是否升级?",
|
||||
title: t(
|
||||
"core.plugin.upload_modal.operations.existed_during_installation.title"
|
||||
),
|
||||
description: t(
|
||||
"core.plugin.upload_modal.operations.existed_during_installation.description"
|
||||
),
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
await apiClient.plugin.upgradePlugin({
|
||||
name: body.pluginName,
|
||||
file: file.data as File,
|
||||
});
|
||||
|
||||
Toast.success("升级成功");
|
||||
Toast.success(t("core.common.toast.upgrade_success"));
|
||||
|
||||
window.location.reload();
|
||||
},
|
||||
|
|
|
@ -4,6 +4,7 @@ import type { Plugin } from "@halo-dev/api-client";
|
|||
import cloneDeep from "lodash.clonedeep";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { Dialog, Toast } from "@halo-dev/components";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
interface usePluginLifeCycleReturn {
|
||||
isStarted: ComputedRef<boolean | undefined>;
|
||||
|
@ -14,6 +15,8 @@ interface usePluginLifeCycleReturn {
|
|||
export function usePluginLifeCycle(
|
||||
plugin?: Ref<Plugin | undefined>
|
||||
): usePluginLifeCycleReturn {
|
||||
const { t } = useI18n();
|
||||
|
||||
const isStarted = computed(() => {
|
||||
return (
|
||||
plugin?.value?.status?.phase === "STARTED" && plugin.value?.spec.enabled
|
||||
|
@ -26,7 +29,11 @@ export function usePluginLifeCycle(
|
|||
const pluginToUpdate = cloneDeep(plugin.value);
|
||||
|
||||
Dialog.info({
|
||||
title: `确定要${pluginToUpdate.spec.enabled ? "停止" : "启动"}该插件吗?`,
|
||||
title: pluginToUpdate.spec.enabled
|
||||
? t("core.plugin.operations.change_status.inactive_title")
|
||||
: t("core.plugin.operations.change_status.active_title"),
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
pluginToUpdate.spec.enabled = !pluginToUpdate.spec.enabled;
|
||||
|
@ -35,7 +42,11 @@ export function usePluginLifeCycle(
|
|||
plugin: pluginToUpdate,
|
||||
});
|
||||
|
||||
Toast.success(`${pluginToUpdate.spec.enabled ? "启动" : "停止"}成功`);
|
||||
Toast.success(
|
||||
pluginToUpdate.spec.enabled
|
||||
? t("core.common.toast.active_success")
|
||||
: t("core.common.toast.inactive_success")
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
|
@ -53,16 +64,21 @@ export function usePluginLifeCycle(
|
|||
Dialog.warning({
|
||||
title: `${
|
||||
deleteExtensions
|
||||
? "确定要卸载该插件以及对应的配置吗?"
|
||||
: "确定要卸载该插件吗?"
|
||||
? t("core.plugin.operations.uninstall_and_delete_config.title")
|
||||
: t("core.plugin.operations.uninstall.title")
|
||||
}`,
|
||||
description: `${
|
||||
enabled
|
||||
? "当前插件还在启用状态,将在停止运行后卸载,该操作不可恢复。"
|
||||
: "该操作不可恢复。"
|
||||
? t("core.plugin.operations.uninstall_when_enabled.description")
|
||||
: t("core.common.dialog.descriptions.cannot_be_recovered")
|
||||
}`,
|
||||
confirmType: "danger",
|
||||
confirmText: `${enabled ? "停止运行并卸载" : "卸载"}`,
|
||||
confirmText: `${
|
||||
enabled
|
||||
? t("core.plugin.operations.uninstall_when_enabled.confirm_text")
|
||||
: t("core.common.buttons.uninstall")
|
||||
}`,
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
if (!plugin.value) return;
|
||||
|
||||
|
@ -107,7 +123,7 @@ export function usePluginLifeCycle(
|
|||
}
|
||||
}
|
||||
|
||||
Toast.success("卸载成功");
|
||||
Toast.success(t("core.common.toast.uninstall_success"));
|
||||
} catch (e) {
|
||||
console.error("Failed to uninstall plugin", e);
|
||||
} finally {
|
||||
|
|
|
@ -21,8 +21,10 @@ import BasicLayout from "@/layouts/BasicLayout.vue";
|
|||
import type { Ref } from "vue";
|
||||
import type { Plugin, Setting, SettingForm } from "@halo-dev/api-client";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
const { t } = useI18n();
|
||||
|
||||
interface PluginTab {
|
||||
id: string;
|
||||
|
@ -36,7 +38,7 @@ interface PluginTab {
|
|||
const initialTabs: PluginTab[] = [
|
||||
{
|
||||
id: "detail",
|
||||
label: "详情",
|
||||
label: t("core.plugin.tabs.detail"),
|
||||
route: {
|
||||
name: "PluginDetail",
|
||||
},
|
||||
|
|
|
@ -24,11 +24,11 @@ export default definePlugin({
|
|||
name: "Plugins",
|
||||
component: PluginList,
|
||||
meta: {
|
||||
title: "插件",
|
||||
title: "core.plugin.title",
|
||||
searchable: true,
|
||||
permissions: ["system:plugins:view"],
|
||||
menu: {
|
||||
name: "插件",
|
||||
name: "core.sidebar.menu.items.plugins",
|
||||
group: "system",
|
||||
icon: markRaw(IconPlug),
|
||||
priority: 0,
|
||||
|
@ -46,7 +46,7 @@ export default definePlugin({
|
|||
name: "PluginDetail",
|
||||
component: PluginDetail,
|
||||
meta: {
|
||||
title: "插件详情",
|
||||
title: "core.plugin.detail.title",
|
||||
permissions: ["system:plugins:view"],
|
||||
},
|
||||
},
|
||||
|
@ -55,7 +55,7 @@ export default definePlugin({
|
|||
name: "PluginSetting",
|
||||
component: PluginSetting,
|
||||
meta: {
|
||||
title: "插件设置",
|
||||
title: "core.plugin.settings.title",
|
||||
permissions: ["system:plugins:manage"],
|
||||
},
|
||||
},
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue