mirror of https://github.com/halo-dev/halo
feat: add attachment management support (halo-dev/console#600)
<!-- Thanks for sending a pull request! Here are some tips for you: 1. 如果这是你的第一次,请阅读我们的贡献指南:<https://github.com/halo-dev/halo/blob/master/CONTRIBUTING.md>。 1. If this is your first time, please read our contributor guidelines: <https://github.com/halo-dev/halo/blob/master/CONTRIBUTING.md>. 2. 请根据你解决问题的类型为 Pull Request 添加合适的标签。 2. Please label this pull request according to what type of issue you are addressing, especially if this is a release targeted pull request. 3. 请确保你已经添加并运行了适当的测试。 3. Ensure you have added or ran the appropriate tests for your PR. --> #### What type of PR is this? /kind feature /milestone 2.0 <!-- 添加其中一个类别: Add one of the following kinds: /kind bug /kind cleanup /kind documentation /kind feature /kind optimization 适当添加其中一个或多个类别(可选): Optionally add one or more of the following kinds if applicable: /kind api-change /kind deprecation /kind failing-test /kind flake /kind regression --> #### What this PR does / why we need it: 增加附件管理的功能,适配 https://github.com/halo-dev/halo/pull/2354 #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/2330 <!-- PR 合并时自动关闭 issue。 Automatically closes linked issue when PR is merged. 用法:`Fixes #<issue 号>`,或者 `Fixes (粘贴 issue 完整链接)` Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`. --> #### Screenshots: None <!-- 如果此 PR 有 UI 的改动,最好截图说明这个 PR 的改动。 If there are UI changes to this PR, it is best to take a screenshot to illustrate the changes to this PR. eg. Before: ![screenshot-before](https://user-images.githubusercontent.com/screenshot.png) After: ![screenshot-after](https://user-images.githubusercontent.com/screenshot.png) --> #### Special notes for your reviewer: todo list: - [x] 根据分组筛选附件列表。 - [x] 非图片文件支持显示占位图。 - [x] 完善选择附件组件。 - [ ] ~~附件引用关系查询。~~ #### Does this PR introduce a user-facing change? <!-- 如果当前 Pull Request 的修改不会造成用户侧的任何变更,在 `release-note` 代码块儿中填写 `NONE`。 否则请填写用户侧能够理解的 Release Note。如果当前 Pull Request 包含破坏性更新(Break Change), Release Note 需要以 `action required` 开头。 If no, just write "NONE" in the release-note block below. If yes, a release note is required: Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required". --> ```release-note None ```pull/3445/head
parent
fdd89f1cf0
commit
3af96040e6
|
@ -33,7 +33,7 @@
|
||||||
"@formkit/themes": "1.0.0-beta.10",
|
"@formkit/themes": "1.0.0-beta.10",
|
||||||
"@formkit/vue": "1.0.0-beta.10",
|
"@formkit/vue": "1.0.0-beta.10",
|
||||||
"@halo-dev/admin-shared": "workspace:*",
|
"@halo-dev/admin-shared": "workspace:*",
|
||||||
"@halo-dev/api-client": "^0.0.12",
|
"@halo-dev/api-client": "^0.0.13",
|
||||||
"@halo-dev/components": "workspace:*",
|
"@halo-dev/components": "workspace:*",
|
||||||
"@halo-dev/richtext-editor": "^0.0.0-alpha.5",
|
"@halo-dev/richtext-editor": "^0.0.0-alpha.5",
|
||||||
"@tiptap/extension-character-count": "2.0.0-beta.31",
|
"@tiptap/extension-character-count": "2.0.0-beta.31",
|
||||||
|
@ -44,12 +44,14 @@
|
||||||
"colorjs.io": "^0.4.0",
|
"colorjs.io": "^0.4.0",
|
||||||
"dayjs": "^1.11.5",
|
"dayjs": "^1.11.5",
|
||||||
"filepond": "^4.30.4",
|
"filepond": "^4.30.4",
|
||||||
"filepond-plugin-image-preview": "^4.6.11",
|
|
||||||
"floating-vue": "2.0.0-beta.19",
|
"floating-vue": "2.0.0-beta.19",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
|
"path-browserify": "^1.0.1",
|
||||||
"pinia": "^2.0.20",
|
"pinia": "^2.0.20",
|
||||||
|
"pretty-bytes": "^6.0.0",
|
||||||
"qs": "^6.11.0",
|
"qs": "^6.11.0",
|
||||||
|
"unsplash-js": "^7.0.15",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"vue": "^3.2.37",
|
"vue": "^3.2.37",
|
||||||
"vue-filepond": "^7.0.3",
|
"vue-filepond": "^7.0.3",
|
||||||
|
@ -60,6 +62,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@changesets/cli": "^2.24.3",
|
"@changesets/cli": "^2.24.3",
|
||||||
|
"@iconify-json/vscode-icons": "^1.1.11",
|
||||||
"@rushstack/eslint-patch": "^1.1.4",
|
"@rushstack/eslint-patch": "^1.1.4",
|
||||||
"@tailwindcss/aspect-ratio": "^0.4.0",
|
"@tailwindcss/aspect-ratio": "^0.4.0",
|
||||||
"@types/jsdom": "^20.0.0",
|
"@types/jsdom": "^20.0.0",
|
||||||
|
@ -93,6 +96,7 @@
|
||||||
"tailwindcss-safe-area": "^0.2.2",
|
"tailwindcss-safe-area": "^0.2.2",
|
||||||
"tailwindcss-themer": "^2.0.1",
|
"tailwindcss-themer": "^2.0.1",
|
||||||
"typescript": "~4.7.4",
|
"typescript": "~4.7.4",
|
||||||
|
"unplugin-icons": "^0.14.8",
|
||||||
"vite": "^3.0.9",
|
"vite": "^3.0.9",
|
||||||
"vite-compression-plugin": "^0.0.4",
|
"vite-compression-plugin": "^0.0.4",
|
||||||
"vite-plugin-externals": "^0.5.1",
|
"vite-plugin-externals": "^0.5.1",
|
||||||
|
|
|
@ -7,15 +7,19 @@ const props = withDefaults(
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
title?: string;
|
title?: string;
|
||||||
width?: number;
|
width?: number;
|
||||||
|
height?: string;
|
||||||
fullscreen?: boolean;
|
fullscreen?: boolean;
|
||||||
bodyClass?: string[];
|
bodyClass?: string[];
|
||||||
|
mountToBody?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
visible: false,
|
visible: false,
|
||||||
title: undefined,
|
title: undefined,
|
||||||
width: 500,
|
width: 500,
|
||||||
|
height: undefined,
|
||||||
fullscreen: false,
|
fullscreen: false,
|
||||||
bodyClass: undefined,
|
bodyClass: undefined,
|
||||||
|
mountToBody: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -36,6 +40,7 @@ const wrapperClasses = computed(() => {
|
||||||
const contentStyles = computed(() => {
|
const contentStyles = computed(() => {
|
||||||
return {
|
return {
|
||||||
maxWidth: props.width + "px",
|
maxWidth: props.width + "px",
|
||||||
|
height: props.height,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -56,7 +61,7 @@ watch(
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<Teleport :disabled="true" to="body">
|
<Teleport :disabled="!mountToBody" to="body">
|
||||||
<div
|
<div
|
||||||
v-show="rootVisible"
|
v-show="rootVisible"
|
||||||
ref="modelWrapper"
|
ref="modelWrapper"
|
||||||
|
@ -65,7 +70,7 @@ watch(
|
||||||
class="modal-wrapper"
|
class="modal-wrapper"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@keyup.esc="handleClose()"
|
@keyup.esc.stop="handleClose()"
|
||||||
>
|
>
|
||||||
<transition
|
<transition
|
||||||
enter-active-class="ease-out duration-200"
|
enter-active-class="ease-out duration-200"
|
||||||
|
@ -77,7 +82,7 @@ watch(
|
||||||
@before-enter="rootVisible = true"
|
@before-enter="rootVisible = true"
|
||||||
@after-leave="rootVisible = false"
|
@after-leave="rootVisible = false"
|
||||||
>
|
>
|
||||||
<div v-show="visible" class="modal-layer" @click="handleClose()" />
|
<div v-show="visible" class="modal-layer" @click.stop="handleClose()" />
|
||||||
</transition>
|
</transition>
|
||||||
<transition
|
<transition
|
||||||
enter-active-class="ease-out duration-200"
|
enter-active-class="ease-out duration-200"
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
"homepage": "https://github.com/halo-dev/halo-admin/tree/next/shared/components#readme",
|
"homepage": "https://github.com/halo-dev/halo-admin/tree/next/shared/components#readme",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@halo-dev/api-client": "^0.0.12",
|
"@halo-dev/api-client": "^0.0.13",
|
||||||
"@halo-dev/components": "workspace:*",
|
"@halo-dev/components": "workspace:*",
|
||||||
"axios": "^0.27.2"
|
"axios": "^0.27.2"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
// core libs
|
// core libs
|
||||||
|
// types
|
||||||
|
import type { Ref } from "vue";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { apiClient } from "../utils/api-client";
|
import { apiClient } from "../utils/api-client";
|
||||||
|
|
||||||
// libs
|
// libs
|
||||||
import cloneDeep from "lodash.clonedeep";
|
import cloneDeep from "lodash.clonedeep";
|
||||||
|
|
||||||
// types
|
|
||||||
import type { Ref } from "vue";
|
|
||||||
import type { FormKitSetting, FormKitSettingSpec } from "../types/formkit";
|
import type { FormKitSetting, FormKitSettingSpec } from "../types/formkit";
|
||||||
import type { ConfigMap } from "@halo-dev/api-client";
|
import type { ConfigMap } from "@halo-dev/api-client";
|
||||||
|
|
||||||
|
@ -19,10 +18,21 @@ const initialConfigMap: ConfigMap = {
|
||||||
data: {},
|
data: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface useSettingFormReturn {
|
||||||
|
settings: Ref<FormKitSetting | undefined>;
|
||||||
|
configMap: Ref<ConfigMap>;
|
||||||
|
configMapFormData: Ref<Record<string, Record<string, string>> | undefined>;
|
||||||
|
saving: Ref<boolean>;
|
||||||
|
handleFetchSettings: () => void;
|
||||||
|
handleFetchConfigMap: () => void;
|
||||||
|
handleSaveConfigMap: () => void;
|
||||||
|
handleReset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
export function useSettingForm(
|
export function useSettingForm(
|
||||||
settingName: Ref<string | undefined>,
|
settingName: Ref<string | undefined>,
|
||||||
configMapName: Ref<string | undefined>
|
configMapName: Ref<string | undefined>
|
||||||
) {
|
): useSettingFormReturn {
|
||||||
const settings = ref<FormKitSetting | undefined>();
|
const settings = ref<FormKitSetting | undefined>();
|
||||||
const configMap = ref<ConfigMap>(cloneDeep(initialConfigMap));
|
const configMap = ref<ConfigMap>(cloneDeep(initialConfigMap));
|
||||||
const configMapFormData = ref<
|
const configMapFormData = ref<
|
||||||
|
@ -113,6 +123,12 @@ export function useSettingForm(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
settings.value = undefined;
|
||||||
|
configMap.value = cloneDeep(initialConfigMap);
|
||||||
|
configMapFormData.value = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
settings,
|
settings,
|
||||||
configMap,
|
configMap,
|
||||||
|
@ -121,5 +137,6 @@ export function useSettingForm(
|
||||||
handleFetchSettings,
|
handleFetchSettings,
|
||||||
handleFetchConfigMap,
|
handleFetchConfigMap,
|
||||||
handleSaveConfigMap,
|
handleSaveConfigMap,
|
||||||
|
handleReset,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ export * from "./types/menus";
|
||||||
export * from "./types/formkit";
|
export * from "./types/formkit";
|
||||||
export * from "./core/plugins";
|
export * from "./core/plugins";
|
||||||
export * from "./states/pages";
|
export * from "./states/pages";
|
||||||
|
export * from "./states/attachment-selector";
|
||||||
export * from "./layouts";
|
export * from "./layouts";
|
||||||
|
|
||||||
export * from "./utils/api-client";
|
export * from "./utils/api-client";
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
import type { Attachment } from "@halo-dev/api-client";
|
||||||
|
import type { Component } from "vue";
|
||||||
|
|
||||||
|
export interface AttachmentSelectorPublicState {
|
||||||
|
providers: AttachmentProvider[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AttachmentLike =
|
||||||
|
| Attachment
|
||||||
|
| string
|
||||||
|
| {
|
||||||
|
url: string;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface AttachmentProvider {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
component: Component | string;
|
||||||
|
callback?: (attachments: AttachmentLike[]) => void;
|
||||||
|
}
|
|
@ -2,10 +2,13 @@ import type { Component, Ref } from "vue";
|
||||||
import type { RouteRecordRaw, RouteRecordName } from "vue-router";
|
import type { RouteRecordRaw, RouteRecordName } from "vue-router";
|
||||||
import type { MenuGroupType } from "./menus";
|
import type { MenuGroupType } from "./menus";
|
||||||
import type { PagesPublicState } from "../states/pages";
|
import type { PagesPublicState } from "../states/pages";
|
||||||
|
import type { AttachmentSelectorPublicState } from "../states/attachment-selector";
|
||||||
|
|
||||||
export type ExtensionPointName = "PAGES" | "POSTS";
|
export type ExtensionPointName = "PAGES" | "POSTS" | "ATTACHMENT_SELECTOR";
|
||||||
|
|
||||||
export type ExtensionPointState = PagesPublicState;
|
export type ExtensionPointState =
|
||||||
|
| PagesPublicState
|
||||||
|
| AttachmentSelectorPublicState;
|
||||||
|
|
||||||
interface RouteRecordAppend {
|
interface RouteRecordAppend {
|
||||||
parentName: RouteRecordName;
|
parentName: RouteRecordName;
|
||||||
|
|
|
@ -12,6 +12,10 @@ import {
|
||||||
ContentHaloRunV1alpha1TagApi,
|
ContentHaloRunV1alpha1TagApi,
|
||||||
PluginHaloRunV1alpha1PluginApi,
|
PluginHaloRunV1alpha1PluginApi,
|
||||||
PluginHaloRunV1alpha1ReverseProxyApi,
|
PluginHaloRunV1alpha1ReverseProxyApi,
|
||||||
|
StorageHaloRunV1alpha1AttachmentApi,
|
||||||
|
StorageHaloRunV1alpha1GroupApi,
|
||||||
|
StorageHaloRunV1alpha1PolicyApi,
|
||||||
|
StorageHaloRunV1alpha1PolicyTemplateApi,
|
||||||
ThemeHaloRunV1alpha1ThemeApi,
|
ThemeHaloRunV1alpha1ThemeApi,
|
||||||
V1alpha1ConfigMapApi,
|
V1alpha1ConfigMapApi,
|
||||||
V1alpha1MenuApi,
|
V1alpha1MenuApi,
|
||||||
|
@ -79,6 +83,20 @@ function setupApiClient(axios: AxiosInstance) {
|
||||||
snapshot: new ContentHaloRunV1alpha1SnapshotApi(undefined, apiUrl, axios),
|
snapshot: new ContentHaloRunV1alpha1SnapshotApi(undefined, apiUrl, axios),
|
||||||
comment: new ContentHaloRunV1alpha1CommentApi(undefined, apiUrl, axios),
|
comment: new ContentHaloRunV1alpha1CommentApi(undefined, apiUrl, axios),
|
||||||
reply: new ContentHaloRunV1alpha1ReplyApi(undefined, apiUrl, axios),
|
reply: new ContentHaloRunV1alpha1ReplyApi(undefined, apiUrl, axios),
|
||||||
|
storage: {
|
||||||
|
group: new StorageHaloRunV1alpha1GroupApi(undefined, apiUrl, axios),
|
||||||
|
attachment: new StorageHaloRunV1alpha1AttachmentApi(
|
||||||
|
undefined,
|
||||||
|
apiUrl,
|
||||||
|
axios
|
||||||
|
),
|
||||||
|
policy: new StorageHaloRunV1alpha1PolicyApi(undefined, apiUrl, axios),
|
||||||
|
policyTemplate: new StorageHaloRunV1alpha1PolicyTemplateApi(
|
||||||
|
undefined,
|
||||||
|
apiUrl,
|
||||||
|
axios
|
||||||
|
),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
// custom endpoints
|
// custom endpoints
|
||||||
user: new ApiHaloRunV1alpha1UserApi(undefined, apiUrl, axios),
|
user: new ApiHaloRunV1alpha1UserApi(undefined, apiUrl, axios),
|
||||||
|
|
114
pnpm-lock.yaml
114
pnpm-lock.yaml
|
@ -13,9 +13,10 @@ importers:
|
||||||
'@formkit/themes': 1.0.0-beta.10
|
'@formkit/themes': 1.0.0-beta.10
|
||||||
'@formkit/vue': 1.0.0-beta.10
|
'@formkit/vue': 1.0.0-beta.10
|
||||||
'@halo-dev/admin-shared': workspace:*
|
'@halo-dev/admin-shared': workspace:*
|
||||||
'@halo-dev/api-client': ^0.0.12
|
'@halo-dev/api-client': ^0.0.13
|
||||||
'@halo-dev/components': workspace:*
|
'@halo-dev/components': workspace:*
|
||||||
'@halo-dev/richtext-editor': ^0.0.0-alpha.5
|
'@halo-dev/richtext-editor': ^0.0.0-alpha.5
|
||||||
|
'@iconify-json/vscode-icons': ^1.1.11
|
||||||
'@rushstack/eslint-patch': ^1.1.4
|
'@rushstack/eslint-patch': ^1.1.4
|
||||||
'@tailwindcss/aspect-ratio': ^0.4.0
|
'@tailwindcss/aspect-ratio': ^0.4.0
|
||||||
'@tiptap/extension-character-count': 2.0.0-beta.31
|
'@tiptap/extension-character-count': 2.0.0-beta.31
|
||||||
|
@ -46,16 +47,17 @@ importers:
|
||||||
eslint-plugin-cypress: ^2.12.1
|
eslint-plugin-cypress: ^2.12.1
|
||||||
eslint-plugin-vue: ^9.3.0
|
eslint-plugin-vue: ^9.3.0
|
||||||
filepond: ^4.30.4
|
filepond: ^4.30.4
|
||||||
filepond-plugin-image-preview: ^4.6.11
|
|
||||||
floating-vue: 2.0.0-beta.19
|
floating-vue: 2.0.0-beta.19
|
||||||
husky: ^8.0.1
|
husky: ^8.0.1
|
||||||
jsdom: ^20.0.0
|
jsdom: ^20.0.0
|
||||||
lodash.clonedeep: ^4.5.0
|
lodash.clonedeep: ^4.5.0
|
||||||
lodash.isequal: ^4.5.0
|
lodash.isequal: ^4.5.0
|
||||||
|
path-browserify: ^1.0.1
|
||||||
pinia: ^2.0.20
|
pinia: ^2.0.20
|
||||||
postcss: ^8.4.16
|
postcss: ^8.4.16
|
||||||
prettier: ^2.7.1
|
prettier: ^2.7.1
|
||||||
prettier-plugin-tailwindcss: ^0.1.13
|
prettier-plugin-tailwindcss: ^0.1.13
|
||||||
|
pretty-bytes: ^6.0.0
|
||||||
qs: ^6.11.0
|
qs: ^6.11.0
|
||||||
sass: ^1.54.5
|
sass: ^1.54.5
|
||||||
start-server-and-test: ^1.14.0
|
start-server-and-test: ^1.14.0
|
||||||
|
@ -63,6 +65,8 @@ importers:
|
||||||
tailwindcss-safe-area: ^0.2.2
|
tailwindcss-safe-area: ^0.2.2
|
||||||
tailwindcss-themer: ^2.0.1
|
tailwindcss-themer: ^2.0.1
|
||||||
typescript: ~4.7.4
|
typescript: ~4.7.4
|
||||||
|
unplugin-icons: ^0.14.8
|
||||||
|
unsplash-js: ^7.0.15
|
||||||
uuid: ^8.3.2
|
uuid: ^8.3.2
|
||||||
vite: ^3.0.9
|
vite: ^3.0.9
|
||||||
vite-compression-plugin: ^0.0.4
|
vite-compression-plugin: ^0.0.4
|
||||||
|
@ -88,7 +92,7 @@ importers:
|
||||||
'@formkit/themes': 1.0.0-beta.10_tailwindcss@3.1.8
|
'@formkit/themes': 1.0.0-beta.10_tailwindcss@3.1.8
|
||||||
'@formkit/vue': 1.0.0-beta.10_wwmyxdjqen5bmh3tr2meig5lki
|
'@formkit/vue': 1.0.0-beta.10_wwmyxdjqen5bmh3tr2meig5lki
|
||||||
'@halo-dev/admin-shared': link:packages/shared
|
'@halo-dev/admin-shared': link:packages/shared
|
||||||
'@halo-dev/api-client': 0.0.12
|
'@halo-dev/api-client': 0.0.13
|
||||||
'@halo-dev/components': link:packages/components
|
'@halo-dev/components': link:packages/components
|
||||||
'@halo-dev/richtext-editor': 0.0.0-alpha.5_vue@3.2.37
|
'@halo-dev/richtext-editor': 0.0.0-alpha.5_vue@3.2.37
|
||||||
'@tiptap/extension-character-count': 2.0.0-beta.31
|
'@tiptap/extension-character-count': 2.0.0-beta.31
|
||||||
|
@ -99,12 +103,14 @@ importers:
|
||||||
colorjs.io: 0.4.0
|
colorjs.io: 0.4.0
|
||||||
dayjs: 1.11.5
|
dayjs: 1.11.5
|
||||||
filepond: 4.30.4
|
filepond: 4.30.4
|
||||||
filepond-plugin-image-preview: 4.6.11_filepond@4.30.4
|
|
||||||
floating-vue: 2.0.0-beta.19_vue@3.2.37
|
floating-vue: 2.0.0-beta.19_vue@3.2.37
|
||||||
lodash.clonedeep: 4.5.0
|
lodash.clonedeep: 4.5.0
|
||||||
lodash.isequal: 4.5.0
|
lodash.isequal: 4.5.0
|
||||||
|
path-browserify: 1.0.1
|
||||||
pinia: 2.0.20_j6bzmzd4ujpabbp5objtwxyjp4
|
pinia: 2.0.20_j6bzmzd4ujpabbp5objtwxyjp4
|
||||||
|
pretty-bytes: 6.0.0
|
||||||
qs: 6.11.0
|
qs: 6.11.0
|
||||||
|
unsplash-js: 7.0.15
|
||||||
uuid: 8.3.2
|
uuid: 8.3.2
|
||||||
vue: 3.2.37
|
vue: 3.2.37
|
||||||
vue-filepond: 7.0.3_filepond@4.30.4+vue@3.2.37
|
vue-filepond: 7.0.3_filepond@4.30.4+vue@3.2.37
|
||||||
|
@ -114,6 +120,7 @@ importers:
|
||||||
yaml: 2.1.1
|
yaml: 2.1.1
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@changesets/cli': 2.24.3
|
'@changesets/cli': 2.24.3
|
||||||
|
'@iconify-json/vscode-icons': 1.1.11
|
||||||
'@rushstack/eslint-patch': 1.1.4
|
'@rushstack/eslint-patch': 1.1.4
|
||||||
'@tailwindcss/aspect-ratio': 0.4.0_tailwindcss@3.1.8
|
'@tailwindcss/aspect-ratio': 0.4.0_tailwindcss@3.1.8
|
||||||
'@types/jsdom': 20.0.0
|
'@types/jsdom': 20.0.0
|
||||||
|
@ -147,6 +154,7 @@ importers:
|
||||||
tailwindcss-safe-area: 0.2.2
|
tailwindcss-safe-area: 0.2.2
|
||||||
tailwindcss-themer: 2.0.1_tailwindcss@3.1.8
|
tailwindcss-themer: 2.0.1_tailwindcss@3.1.8
|
||||||
typescript: 4.7.4
|
typescript: 4.7.4
|
||||||
|
unplugin-icons: 0.14.8_jz6tpbhhn2upnbiwxxr6wx7age
|
||||||
vite: 3.0.9_sass@1.54.5
|
vite: 3.0.9_sass@1.54.5
|
||||||
vite-compression-plugin: 0.0.4
|
vite-compression-plugin: 0.0.4
|
||||||
vite-plugin-externals: 0.5.1_vite@3.0.9
|
vite-plugin-externals: 0.5.1_vite@3.0.9
|
||||||
|
@ -186,12 +194,12 @@ importers:
|
||||||
|
|
||||||
packages/shared:
|
packages/shared:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@halo-dev/api-client': ^0.0.12
|
'@halo-dev/api-client': ^0.0.13
|
||||||
'@halo-dev/components': workspace:*
|
'@halo-dev/components': workspace:*
|
||||||
axios: ^0.27.2
|
axios: ^0.27.2
|
||||||
vite-plugin-dts: ^1.4.1
|
vite-plugin-dts: ^1.4.1
|
||||||
dependencies:
|
dependencies:
|
||||||
'@halo-dev/api-client': 0.0.12
|
'@halo-dev/api-client': 0.0.13
|
||||||
'@halo-dev/components': link:../components
|
'@halo-dev/components': link:../components
|
||||||
axios: 0.27.2
|
axios: 0.27.2
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
@ -2117,8 +2125,8 @@ packages:
|
||||||
- windicss
|
- windicss
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@halo-dev/api-client/0.0.12:
|
/@halo-dev/api-client/0.0.13:
|
||||||
resolution: {integrity: sha512-fOI3DB9rOA1Z+h1aKiEQ+2kWkNSmdWIDvd+39dR5b3X0DmKH+zWrNmygA5Qe2gBPX28TNt/zr2qCKUjGjb99CA==}
|
resolution: {integrity: sha512-RP7f8OaB2JS9y6diJpjozhxo/tx5CaQD2FpWj9udoSsWheySR7Tc+wqOMgkhP51xQskhTXRmV9m2pOg6qzFKwA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@halo-dev/richtext-editor/0.0.0-alpha.5_vue@3.2.37:
|
/@halo-dev/richtext-editor/0.0.0-alpha.5_vue@3.2.37:
|
||||||
|
@ -2243,6 +2251,12 @@ packages:
|
||||||
'@iconify/types': 1.1.0
|
'@iconify/types': 1.1.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@iconify-json/vscode-icons/1.1.11:
|
||||||
|
resolution: {integrity: sha512-2HducOcHAGEY5fD9NRilS4BeLTu72/JEjKy9/HsUzst9VlXT0TGH6qaU1m7X2bSFZHV+CGsisG1lq6M5XxzytQ==}
|
||||||
|
dependencies:
|
||||||
|
'@iconify/types': 1.1.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@iconify/types/1.1.0:
|
/@iconify/types/1.1.0:
|
||||||
resolution: {integrity: sha512-Jh0llaK2LRXQoYsorIH8maClebsnzTcve+7U3rQUSnC11X4jtPnFuyatqFLvMxZ8MLG8dB4zfHsbPfuvxluONw==}
|
resolution: {integrity: sha512-Jh0llaK2LRXQoYsorIH8maClebsnzTcve+7U3rQUSnC11X4jtPnFuyatqFLvMxZ8MLG8dB4zfHsbPfuvxluONw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -3120,6 +3134,10 @@ packages:
|
||||||
'@types/node': 17.0.45
|
'@types/node': 17.0.45
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/content-type/1.1.5:
|
||||||
|
resolution: {integrity: sha512-dgMN+syt1xb7Hk8LU6AODOfPlvz5z1CbXpPuJE5ZrX9STfBOIXF09pEB8N7a97WT9dbngt3ksDCm6GW6yMrxfQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/estree/0.0.39:
|
/@types/estree/0.0.39:
|
||||||
resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==}
|
resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -4425,6 +4443,11 @@ packages:
|
||||||
resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==}
|
resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/content-type/1.0.4:
|
||||||
|
resolution: {integrity: sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/convert-source-map/1.8.0:
|
/convert-source-map/1.8.0:
|
||||||
resolution: {integrity: sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==}
|
resolution: {integrity: sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -5527,14 +5550,6 @@ packages:
|
||||||
minimatch: 5.0.1
|
minimatch: 5.0.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/filepond-plugin-image-preview/4.6.11_filepond@4.30.4:
|
|
||||||
resolution: {integrity: sha512-0EmQ9HnOb/X0xc5rLcNRhhmdUbp7oiicRwQrcr90ZfVmPJOOZoX3ZGUEsEPj7luMI55huguhcVozdESxtqnuRw==}
|
|
||||||
peerDependencies:
|
|
||||||
filepond: '>=4.x <5.x'
|
|
||||||
dependencies:
|
|
||||||
filepond: 4.30.4
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/filepond/4.30.4:
|
/filepond/4.30.4:
|
||||||
resolution: {integrity: sha512-FCwsMvG9iiEs6uobdDrTaKsCgsqys0NuLgPPD8n37AYVYBiiDkrPkk9MSIU5rT2FahYcL1bScYI9huIPtlzqyA==}
|
resolution: {integrity: sha512-FCwsMvG9iiEs6uobdDrTaKsCgsqys0NuLgPPD8n37AYVYBiiDkrPkk9MSIU5rT2FahYcL1bScYI9huIPtlzqyA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -7243,7 +7258,6 @@ packages:
|
||||||
|
|
||||||
/path-browserify/1.0.1:
|
/path-browserify/1.0.1:
|
||||||
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/path-exists/4.0.0:
|
/path-exists/4.0.0:
|
||||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||||
|
@ -7462,7 +7476,6 @@ packages:
|
||||||
/pretty-bytes/6.0.0:
|
/pretty-bytes/6.0.0:
|
||||||
resolution: {integrity: sha512-6UqkYefdogmzqAZWzJ7laYeJnaXDy2/J+ZqiiMtS7t7OfpXWTlaeGMwX8U6EFvPV/YWWEKRkS8hKS4k60WHTOg==}
|
resolution: {integrity: sha512-6UqkYefdogmzqAZWzJ7laYeJnaXDy2/J+ZqiiMtS7t7OfpXWTlaeGMwX8U6EFvPV/YWWEKRkS8hKS4k60WHTOg==}
|
||||||
engines: {node: ^14.13.1 || >=16.0.0}
|
engines: {node: ^14.13.1 || >=16.0.0}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/process-nextick-args/2.0.1:
|
/process-nextick-args/2.0.1:
|
||||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||||
|
@ -8724,6 +8737,39 @@ packages:
|
||||||
- webpack
|
- webpack
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/unplugin-icons/0.14.8_jz6tpbhhn2upnbiwxxr6wx7age:
|
||||||
|
resolution: {integrity: sha512-YxLC0Uxec+ayl8ju3CXmRX4Jg7IF8Tu2cRyq/okXwMK6fM140SPae332ByTlul1E/I7I0PXYSVVn8SlGunM/2g==}
|
||||||
|
peerDependencies:
|
||||||
|
'@svgr/core': '>=5.5.0'
|
||||||
|
'@vue/compiler-sfc': ^3.0.2
|
||||||
|
vue-template-compiler: ^2.6.12
|
||||||
|
vue-template-es2015-compiler: ^1.9.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@svgr/core':
|
||||||
|
optional: true
|
||||||
|
'@vue/compiler-sfc':
|
||||||
|
optional: true
|
||||||
|
vue-template-compiler:
|
||||||
|
optional: true
|
||||||
|
vue-template-es2015-compiler:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@antfu/install-pkg': 0.1.0
|
||||||
|
'@antfu/utils': 0.5.2
|
||||||
|
'@iconify/utils': 1.0.33
|
||||||
|
'@vue/compiler-sfc': 3.2.37
|
||||||
|
debug: 4.3.4
|
||||||
|
kolorist: 1.5.1
|
||||||
|
local-pkg: 0.4.2
|
||||||
|
unplugin: 0.8.0_vite@3.0.9
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- esbuild
|
||||||
|
- rollup
|
||||||
|
- supports-color
|
||||||
|
- vite
|
||||||
|
- webpack
|
||||||
|
dev: true
|
||||||
|
|
||||||
/unplugin/0.8.0:
|
/unplugin/0.8.0:
|
||||||
resolution: {integrity: sha512-OzOkJ9XOPlD1Cph6qy/p4i/KSUbs76GToXjH/STHpfo6D7y+EqpfAL6G6HaoOw5QLkt9+KWwcxYUmPFkDf1upQ==}
|
resolution: {integrity: sha512-OzOkJ9XOPlD1Cph6qy/p4i/KSUbs76GToXjH/STHpfo6D7y+EqpfAL6G6HaoOw5QLkt9+KWwcxYUmPFkDf1upQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
@ -8747,6 +8793,38 @@ packages:
|
||||||
webpack-virtual-modules: 0.4.4
|
webpack-virtual-modules: 0.4.4
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/unplugin/0.8.0_vite@3.0.9:
|
||||||
|
resolution: {integrity: sha512-OzOkJ9XOPlD1Cph6qy/p4i/KSUbs76GToXjH/STHpfo6D7y+EqpfAL6G6HaoOw5QLkt9+KWwcxYUmPFkDf1upQ==}
|
||||||
|
peerDependencies:
|
||||||
|
esbuild: '>=0.13'
|
||||||
|
rollup: ^2.50.0
|
||||||
|
vite: ^2.3.0 || ^3.0.0-0
|
||||||
|
webpack: 4 || 5
|
||||||
|
peerDependenciesMeta:
|
||||||
|
esbuild:
|
||||||
|
optional: true
|
||||||
|
rollup:
|
||||||
|
optional: true
|
||||||
|
vite:
|
||||||
|
optional: true
|
||||||
|
webpack:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
acorn: 8.8.0
|
||||||
|
chokidar: 3.5.3
|
||||||
|
vite: 3.0.9_sass@1.54.5
|
||||||
|
webpack-sources: 3.2.3
|
||||||
|
webpack-virtual-modules: 0.4.4
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/unsplash-js/7.0.15:
|
||||||
|
resolution: {integrity: sha512-WGqKp9wl2m2tAUPyw2eMZs/KICR+A52tCaRapzVXWxkA4pjHqsaGwiJXTEW7hBy4Pu0QmP6KxTt2jST3tluawA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dependencies:
|
||||||
|
'@types/content-type': 1.1.5
|
||||||
|
content-type: 1.0.4
|
||||||
|
dev: false
|
||||||
|
|
||||||
/untildify/4.0.0:
|
/untildify/4.0.0:
|
||||||
resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==}
|
resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useImage } from "@vueuse/core";
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
src: string;
|
||||||
|
alt?: string;
|
||||||
|
classes?: string | string[];
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
src: "",
|
||||||
|
alt: "",
|
||||||
|
classes: "",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { isLoading, error } = useImage({ src: props.src });
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<template v-if="isLoading">
|
||||||
|
<slot name="loading"> loading... </slot>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="error">
|
||||||
|
<slot name="error"> error </slot>
|
||||||
|
</template>
|
||||||
|
<img v-else :src="src" :alt="alt" :class="classes" />
|
||||||
|
</template>
|
|
@ -0,0 +1,88 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import VueFilePond from "vue-filepond";
|
||||||
|
import "filepond/dist/filepond.min.css";
|
||||||
|
import type { AxiosRequestConfig, AxiosResponse } from "axios";
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
allowMultiple?: boolean;
|
||||||
|
labelIdle?: string;
|
||||||
|
maxFiles?: number | null;
|
||||||
|
maxParallelUploads?: number;
|
||||||
|
name?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
handler: (
|
||||||
|
// eslint-disable-next-line
|
||||||
|
file: any,
|
||||||
|
config: AxiosRequestConfig
|
||||||
|
// eslint-disable-next-line
|
||||||
|
) => Promise<AxiosResponse<any, any>>;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
allowMultiple: false,
|
||||||
|
labelIdle: "Drop file here",
|
||||||
|
maxFiles: null,
|
||||||
|
maxParallelUploads: 3,
|
||||||
|
name: "file",
|
||||||
|
disabled: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: "uploaded", response: AxiosResponse): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const FilePond = VueFilePond();
|
||||||
|
|
||||||
|
const FilePondRef = ref();
|
||||||
|
|
||||||
|
const server = {
|
||||||
|
process: async (fieldName, file, metadata, load, error, progress, abort) => {
|
||||||
|
try {
|
||||||
|
const response = await props.handler(file, {
|
||||||
|
onUploadProgress: (progressEvent) => {
|
||||||
|
if (progressEvent.total > 0) {
|
||||||
|
progress(
|
||||||
|
progressEvent.lengthComputable,
|
||||||
|
progressEvent.loaded,
|
||||||
|
progressEvent.total
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
emit("uploaded", response);
|
||||||
|
load(response);
|
||||||
|
} catch (e) {
|
||||||
|
error(e);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
abort: () => {
|
||||||
|
abort();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveFiles = () => {
|
||||||
|
FilePondRef.value.removeFiles();
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
handleRemoveFiles,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<FilePond
|
||||||
|
ref="FilePondRef"
|
||||||
|
:allow-multiple="allowMultiple"
|
||||||
|
:allow-revert="false"
|
||||||
|
:label-idle="labelIdle"
|
||||||
|
:max-files="maxFiles"
|
||||||
|
:max-parallel-uploads="maxParallelUploads"
|
||||||
|
:name="name"
|
||||||
|
:server="server"
|
||||||
|
:disabled="disabled"
|
||||||
|
></FilePond>
|
||||||
|
</template>
|
|
@ -1,13 +1,12 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {
|
import {
|
||||||
IconAddCircle,
|
|
||||||
IconArrowDown,
|
IconArrowDown,
|
||||||
|
IconArrowLeft,
|
||||||
|
IconArrowRight,
|
||||||
IconCheckboxFill,
|
IconCheckboxFill,
|
||||||
IconDatabase2Line,
|
IconDatabase2Line,
|
||||||
IconGrid,
|
IconGrid,
|
||||||
IconList,
|
IconList,
|
||||||
IconMore,
|
|
||||||
IconPalette,
|
|
||||||
IconSettings,
|
IconSettings,
|
||||||
IconUpload,
|
IconUpload,
|
||||||
VButton,
|
VButton,
|
||||||
|
@ -15,16 +14,143 @@ import {
|
||||||
VPageHeader,
|
VPageHeader,
|
||||||
VPagination,
|
VPagination,
|
||||||
VSpace,
|
VSpace,
|
||||||
VTag,
|
VEmpty,
|
||||||
|
IconCloseCircle,
|
||||||
|
IconFolder,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
|
import LazyImage from "@/components/image/LazyImage.vue";
|
||||||
import AttachmentDetailModal from "./components/AttachmentDetailModal.vue";
|
import AttachmentDetailModal from "./components/AttachmentDetailModal.vue";
|
||||||
import AttachmentUploadModal from "./components/AttachmentUploadModal.vue";
|
import AttachmentUploadModal from "./components/AttachmentUploadModal.vue";
|
||||||
import AttachmentSelectModal from "./components/AttachmentSelectModal.vue";
|
import AttachmentPoliciesModal from "./components/AttachmentPoliciesModal.vue";
|
||||||
import AttachmentStrategiesModal from "./components/AttachmentStrategiesModal.vue";
|
import AttachmentGroupList from "./components/AttachmentGroupList.vue";
|
||||||
import AttachmentGroupEditingModal from "./components/AttachmentGroupEditingModal.vue";
|
import { onMounted, ref } from "vue";
|
||||||
import { ref } from "vue";
|
|
||||||
import { useUserFetch } from "@/modules/system/users/composables/use-user";
|
import { useUserFetch } from "@/modules/system/users/composables/use-user";
|
||||||
|
import type { Attachment, Group, Policy, User } from "@halo-dev/api-client";
|
||||||
|
import { formatDatetime } from "@/utils/date";
|
||||||
|
import prettyBytes from "pretty-bytes";
|
||||||
|
import { useFetchAttachmentPolicy } from "./composables/use-attachment-policy";
|
||||||
|
import { useAttachmentControl } from "./composables/use-attachment";
|
||||||
|
import AttachmentSelectorModal from "@/modules/contents/attachments/components/AttachmentSelectorModal.vue";
|
||||||
|
import AttachmentFileTypeIcon from "./components/AttachmentFileTypeIcon.vue";
|
||||||
|
import { apiClient } from "@halo-dev/admin-shared";
|
||||||
|
import cloneDeep from "lodash.clonedeep";
|
||||||
|
import { isImage } from "@/utils/image";
|
||||||
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
|
import { useFetchAttachmentGroup } from "./composables/use-attachment-group";
|
||||||
|
|
||||||
|
const policyVisible = ref(false);
|
||||||
|
const uploadVisible = ref(false);
|
||||||
|
const detailVisible = ref(false);
|
||||||
|
const selectVisible = ref(false);
|
||||||
|
|
||||||
|
const { users } = useUserFetch();
|
||||||
|
const { policies } = useFetchAttachmentPolicy({ fetchOnMounted: true });
|
||||||
|
const { groups, handleFetchGroups } = useFetchAttachmentGroup({
|
||||||
|
fetchOnMounted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedGroup = ref<Group>();
|
||||||
|
|
||||||
|
// Filter
|
||||||
|
const selectedPolicy = ref<Policy>();
|
||||||
|
const selectedUser = ref<User>();
|
||||||
|
const keyword = ref<string>("");
|
||||||
|
|
||||||
|
function handleSelectPolicy(policy: Policy | undefined) {
|
||||||
|
selectedPolicy.value = policy;
|
||||||
|
handleFetchAttachments();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelectUser(user: User | undefined) {
|
||||||
|
selectedUser.value = user;
|
||||||
|
handleFetchAttachments();
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
attachments,
|
||||||
|
selectedAttachment,
|
||||||
|
selectedAttachments,
|
||||||
|
checkedAll,
|
||||||
|
loading,
|
||||||
|
handleFetchAttachments,
|
||||||
|
handleSelectNext,
|
||||||
|
handleSelectPrevious,
|
||||||
|
handlePaginationChange,
|
||||||
|
handleDeleteInBatch,
|
||||||
|
handleCheckAll,
|
||||||
|
handleSelect,
|
||||||
|
isChecked,
|
||||||
|
handleReset,
|
||||||
|
} = useAttachmentControl({
|
||||||
|
group: selectedGroup,
|
||||||
|
policy: selectedPolicy,
|
||||||
|
user: selectedUser,
|
||||||
|
keyword: keyword,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleMove = async (group: Group) => {
|
||||||
|
try {
|
||||||
|
const promises = Array.from(selectedAttachments.value).map((attachment) => {
|
||||||
|
const attachmentToUpdate = cloneDeep(attachment);
|
||||||
|
attachmentToUpdate.spec.groupRef = {
|
||||||
|
name: group.metadata.name,
|
||||||
|
};
|
||||||
|
return apiClient.extension.storage.attachment.updatestorageHaloRunV1alpha1Attachment(
|
||||||
|
attachment.metadata.name,
|
||||||
|
attachmentToUpdate
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
selectedAttachments.value.clear();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
handleFetchAttachments();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClickItem = (attachment: Attachment) => {
|
||||||
|
if (attachment.metadata.deletionTimestamp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedAttachments.value.size > 0) {
|
||||||
|
handleSelect(attachment);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedAttachment.value = attachment;
|
||||||
|
selectedAttachments.value.clear();
|
||||||
|
detailVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheckAllChange = (e: Event) => {
|
||||||
|
const { checked } = e.target as HTMLInputElement;
|
||||||
|
handleCheckAll(checked);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDetailModalClose = () => {
|
||||||
|
selectedAttachment.value = undefined;
|
||||||
|
handleFetchAttachments();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUploadModalClose = () => {
|
||||||
|
routeQueryAction.value = undefined;
|
||||||
|
handleFetchAttachments();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onGroupChange = () => {
|
||||||
|
handleReset();
|
||||||
|
handleFetchAttachments();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPolicyName = (name: string | undefined) => {
|
||||||
|
const policy = policies.value.find((p) => p.metadata.name === name);
|
||||||
|
return policy?.spec.displayName;
|
||||||
|
};
|
||||||
|
|
||||||
|
// View type
|
||||||
const viewTypes = [
|
const viewTypes = [
|
||||||
{
|
{
|
||||||
name: "list",
|
name: "list",
|
||||||
|
@ -36,78 +162,55 @@ const viewTypes = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const viewType = ref("grid");
|
const viewType = useRouteQuery<string>("view", "grid");
|
||||||
|
|
||||||
const strategyVisible = ref(false);
|
// Route query action
|
||||||
const selectVisible = ref(false);
|
const routeQueryAction = useRouteQuery<string | undefined>("action");
|
||||||
const uploadVisible = ref(false);
|
|
||||||
const detailVisible = ref(false);
|
|
||||||
const groupEditingModal = ref(false);
|
|
||||||
const checkAll = ref(false);
|
|
||||||
|
|
||||||
const { users } = useUserFetch();
|
onMounted(() => {
|
||||||
|
if (!routeQueryAction.value) {
|
||||||
const attachments = Array.from(new Array(50), (_, index) => index).map(
|
return;
|
||||||
(index) => {
|
|
||||||
return {
|
|
||||||
id: index,
|
|
||||||
name: `attachment-${index}`,
|
|
||||||
url: `https://picsum.photos/1000/700?random=${index}`,
|
|
||||||
size: "1.2MB",
|
|
||||||
type: "image/png",
|
|
||||||
strategy: "本地存储",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
);
|
if (routeQueryAction.value === "upload") {
|
||||||
|
uploadVisible.value = true;
|
||||||
const folders = [
|
}
|
||||||
{
|
});
|
||||||
name: "2022",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "2021",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Photos",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Documents",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Videos",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Pictures",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Developer",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<AttachmentDetailModal v-model:visible="detailVisible" />
|
<AttachmentSelectorModal v-model:visible="selectVisible" />
|
||||||
<AttachmentUploadModal v-model:visible="uploadVisible" />
|
<AttachmentDetailModal
|
||||||
<AttachmentSelectModal v-model:visible="selectVisible" />
|
v-model:visible="detailVisible"
|
||||||
<AttachmentStrategiesModal v-model:visible="strategyVisible" />
|
:attachment="selectedAttachment"
|
||||||
<AttachmentGroupEditingModal v-model:visible="groupEditingModal" />
|
@close="onDetailModalClose"
|
||||||
|
>
|
||||||
|
<template #actions>
|
||||||
|
<div class="modal-header-action" @click="handleSelectPrevious">
|
||||||
|
<IconArrowLeft />
|
||||||
|
</div>
|
||||||
|
<div class="modal-header-action" @click="handleSelectNext">
|
||||||
|
<IconArrowRight />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</AttachmentDetailModal>
|
||||||
|
<AttachmentUploadModal
|
||||||
|
v-model:visible="uploadVisible"
|
||||||
|
:group="selectedGroup"
|
||||||
|
@close="onUploadModalClose"
|
||||||
|
/>
|
||||||
|
<AttachmentPoliciesModal v-model:visible="policyVisible" />
|
||||||
<VPageHeader title="附件库">
|
<VPageHeader title="附件库">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<IconPalette class="mr-2 self-center" />
|
<IconFolder class="mr-2 self-center" />
|
||||||
</template>
|
</template>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<VSpace>
|
<VSpace>
|
||||||
<VButton size="sm" @click="strategyVisible = true">
|
<VButton size="sm" @click="selectVisible = true"> 选择附件</VButton>
|
||||||
|
<VButton size="sm" @click="policyVisible = true">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<IconDatabase2Line class="h-full w-full" />
|
<IconDatabase2Line class="h-full w-full" />
|
||||||
</template>
|
</template>
|
||||||
存储策略
|
存储策略
|
||||||
</VButton>
|
</VButton>
|
||||||
<VButton size="sm">
|
|
||||||
<template #icon>
|
|
||||||
<IconSettings class="h-full w-full" />
|
|
||||||
</template>
|
|
||||||
设置
|
|
||||||
</VButton>
|
|
||||||
<VButton type="secondary" @click="uploadVisible = true">
|
<VButton type="secondary" @click="uploadVisible = true">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<IconUpload class="h-full w-full" />
|
<IconUpload class="h-full w-full" />
|
||||||
|
@ -129,20 +232,81 @@ const folders = [
|
||||||
>
|
>
|
||||||
<div class="mr-4 hidden items-center sm:flex">
|
<div class="mr-4 hidden items-center sm:flex">
|
||||||
<input
|
<input
|
||||||
v-model="checkAll"
|
v-model="checkedAll"
|
||||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
|
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
@change="handleCheckAllChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-full flex-1 sm:w-auto">
|
<div class="flex w-full flex-1 items-center sm:w-auto">
|
||||||
<FormKit
|
<div
|
||||||
v-if="!checkAll"
|
v-if="!selectedAttachments.size"
|
||||||
placeholder="输入关键词搜索"
|
class="flex items-center gap-2"
|
||||||
type="text"
|
>
|
||||||
></FormKit>
|
<FormKit
|
||||||
|
v-model="keyword"
|
||||||
|
placeholder="输入关键词搜索"
|
||||||
|
type="text"
|
||||||
|
@keyup.enter="handleFetchAttachments()"
|
||||||
|
></FormKit>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="selectedPolicy"
|
||||||
|
class="group flex cursor-pointer items-center justify-center gap-1 rounded-full bg-gray-200 px-2 py-1 hover:bg-gray-300"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-xs text-gray-600 group-hover:text-gray-900"
|
||||||
|
>
|
||||||
|
存储策略:{{ selectedPolicy?.spec.displayName }}
|
||||||
|
</span>
|
||||||
|
<IconCloseCircle
|
||||||
|
class="h-4 w-4 text-gray-600"
|
||||||
|
@click="handleSelectPolicy(undefined)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="selectedUser"
|
||||||
|
class="group flex cursor-pointer items-center justify-center gap-1 rounded-full bg-gray-200 px-2 py-1 hover:bg-gray-300"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-xs text-gray-600 group-hover:text-gray-900"
|
||||||
|
>
|
||||||
|
上传者:{{ selectedUser?.spec.displayName }}
|
||||||
|
</span>
|
||||||
|
<IconCloseCircle
|
||||||
|
class="h-4 w-4 text-gray-600"
|
||||||
|
@click="handleSelectUser(undefined)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<VSpace v-else>
|
<VSpace v-else>
|
||||||
<VButton type="default">设置</VButton>
|
<VButton type="danger" @click="handleDeleteInBatch">
|
||||||
<VButton type="danger">删除</VButton>
|
删除
|
||||||
|
</VButton>
|
||||||
|
<VButton @click="selectedAttachments.clear()">
|
||||||
|
取消选择
|
||||||
|
</VButton>
|
||||||
|
<FloatingDropdown>
|
||||||
|
<VButton>移动</VButton>
|
||||||
|
<template #popper>
|
||||||
|
<div class="w-72 p-4">
|
||||||
|
<ul class="space-y-1">
|
||||||
|
<li
|
||||||
|
v-for="(group, index) in groups"
|
||||||
|
:key="index"
|
||||||
|
v-close-popper
|
||||||
|
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
||||||
|
@click="handleMove(group)"
|
||||||
|
>
|
||||||
|
<span class="truncate">
|
||||||
|
{{ group.spec.displayName }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</FloatingDropdown>
|
||||||
</VSpace>
|
</VSpace>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 flex sm:mt-0">
|
<div class="mt-4 flex sm:mt-0">
|
||||||
|
@ -160,22 +324,20 @@ const folders = [
|
||||||
<div class="w-72 p-4">
|
<div class="w-72 p-4">
|
||||||
<ul class="space-y-1">
|
<ul class="space-y-1">
|
||||||
<li
|
<li
|
||||||
|
v-for="(policy, index) in policies"
|
||||||
|
:key="index"
|
||||||
v-close-popper
|
v-close-popper
|
||||||
|
:class="{
|
||||||
|
'bg-gray-100':
|
||||||
|
selectedPolicy?.metadata.name ===
|
||||||
|
policy.metadata.name,
|
||||||
|
}"
|
||||||
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
||||||
|
@click="handleSelectPolicy(policy)"
|
||||||
>
|
>
|
||||||
<span class="truncate">本地</span>
|
<span class="truncate">
|
||||||
</li>
|
{{ policy.spec.displayName }}
|
||||||
<li
|
</span>
|
||||||
v-close-popper
|
|
||||||
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
|
||||||
>
|
|
||||||
<span class="truncate">阿里云 OSS</span>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
v-close-popper
|
|
||||||
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
|
||||||
>
|
|
||||||
<span class="truncate">Amazon S3</span>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -191,8 +353,8 @@ const folders = [
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<template #popper>
|
<template #popper>
|
||||||
<div class="h-96 w-80 p-4">
|
<div class="h-96 w-80">
|
||||||
<div class="bg-white">
|
<div class="bg-white p-4">
|
||||||
<!--TODO: Auto Focus-->
|
<!--TODO: Auto Focus-->
|
||||||
<FormKit
|
<FormKit
|
||||||
placeholder="输入关键词搜索"
|
placeholder="输入关键词搜索"
|
||||||
|
@ -205,15 +367,17 @@ const folders = [
|
||||||
v-for="(user, index) in users"
|
v-for="(user, index) in users"
|
||||||
:key="index"
|
:key="index"
|
||||||
v-close-popper
|
v-close-popper
|
||||||
class="cursor-pointer py-4 hover:bg-gray-50"
|
class="cursor-pointer hover:bg-gray-50"
|
||||||
|
:class="{
|
||||||
|
'bg-gray-100':
|
||||||
|
selectedUser?.metadata.name ===
|
||||||
|
user.metadata.name,
|
||||||
|
}"
|
||||||
|
@click="handleSelectUser(user)"
|
||||||
>
|
>
|
||||||
<div class="flex items-center space-x-4">
|
<div
|
||||||
<div class="flex items-center">
|
class="flex items-center space-x-4 px-4 py-3"
|
||||||
<input
|
>
|
||||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
<img
|
<img
|
||||||
:alt="user.spec.displayName"
|
:alt="user.spec.displayName"
|
||||||
|
@ -231,9 +395,6 @@ const folders = [
|
||||||
@{{ user.metadata.name }}
|
@{{ user.metadata.name }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<VTag>{{ index + 1 }} 篇</VTag>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -269,14 +430,6 @@ const folders = [
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</FloatingDropdown>
|
</FloatingDropdown>
|
||||||
<div
|
|
||||||
class="flex cursor-pointer items-center text-sm text-gray-700 hover:text-black"
|
|
||||||
>
|
|
||||||
<span class="mr-0.5">标签</span>
|
|
||||||
<span>
|
|
||||||
<IconArrowDown />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<FloatingDropdown>
|
<FloatingDropdown>
|
||||||
<div
|
<div
|
||||||
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
||||||
|
@ -337,162 +490,229 @@ const folders = [
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="viewType === 'grid'">
|
<div :style="`${viewType === 'list' ? 'padding:12px 16px 0' : ''}`">
|
||||||
<div class="mb-5 grid grid-cols-2 gap-x-2 gap-y-3 sm:grid-cols-6">
|
<AttachmentGroupList
|
||||||
<div
|
v-model:selected-group="selectedGroup"
|
||||||
class="flex cursor-pointer items-center rounded-base bg-gray-200 p-2 text-gray-900 transition-all"
|
@select="onGroupChange"
|
||||||
>
|
@update="handleFetchGroups"
|
||||||
<div class="flex flex-1 items-center">
|
/>
|
||||||
<span class="text-sm">全部(212)</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="flex 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">
|
|
||||||
<span class="text-sm">未分组(18)</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-for="(folder, index) in folders"
|
|
||||||
:key="index"
|
|
||||||
class="flex 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">
|
|
||||||
<span class="text-sm">
|
|
||||||
{{ folder.name }}({{ index * 20 }})
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<FloatingDropdown>
|
|
||||||
<IconMore />
|
|
||||||
<template #popper>
|
|
||||||
<div class="w-48 p-2">
|
|
||||||
<VSpace class="w-full" direction="column">
|
|
||||||
<VButton
|
|
||||||
v-close-popper
|
|
||||||
block
|
|
||||||
type="secondary"
|
|
||||||
@click="groupEditingModal = true"
|
|
||||||
>
|
|
||||||
重命名
|
|
||||||
</VButton>
|
|
||||||
<VButton v-close-popper block type="danger">
|
|
||||||
删除
|
|
||||||
</VButton>
|
|
||||||
</VSpace>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</FloatingDropdown>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="flex 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"
|
|
||||||
@click="groupEditingModal = true"
|
|
||||||
>
|
|
||||||
<div class="flex flex-1 items-center">
|
|
||||||
<span class="text-sm">添加分组</span>
|
|
||||||
</div>
|
|
||||||
<IconAddCircle />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mt-2 grid grid-cols-3 gap-x-2 gap-y-3 sm:grid-cols-3 md:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-12"
|
|
||||||
role="list"
|
|
||||||
>
|
|
||||||
<VCard
|
|
||||||
v-for="(attachment, index) in attachments"
|
|
||||||
:key="index"
|
|
||||||
:body-class="['!p-0']"
|
|
||||||
class="hover:shadow"
|
|
||||||
@click="detailVisible = true"
|
|
||||||
>
|
|
||||||
<div class="relative bg-white">
|
|
||||||
<div
|
|
||||||
class="group aspect-w-10 aspect-h-8 block h-full w-full cursor-pointer overflow-hidden bg-gray-100"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
:src="attachment.url"
|
|
||||||
alt=""
|
|
||||||
class="pointer-events-none object-cover group-hover:opacity-75"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p
|
|
||||||
class="pointer-events-none block truncate px-2 py-1 text-center text-xs font-medium text-gray-700"
|
|
||||||
>
|
|
||||||
{{ attachment.name }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<IconCheckboxFill
|
|
||||||
v-if="checkAll"
|
|
||||||
class="absolute top-0.5 right-0.5"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</VCard>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul
|
<VEmpty
|
||||||
v-if="viewType === 'list'"
|
v-if="!attachments.total && !loading"
|
||||||
class="box-border h-full w-full divide-y divide-gray-100"
|
message="当前分组没有附件,你可以尝试刷新或者上传附件"
|
||||||
role="list"
|
title="当前分组没有附件"
|
||||||
>
|
>
|
||||||
<li v-for="(attachment, index) in attachments" :key="index">
|
<template #actions>
|
||||||
|
<VSpace>
|
||||||
|
<VButton @click="handleFetchAttachments">刷新</VButton>
|
||||||
|
<VButton type="secondary" @click="uploadVisible = true">
|
||||||
|
<template #icon>
|
||||||
|
<IconUpload class="h-full w-full" />
|
||||||
|
</template>
|
||||||
|
上传附件
|
||||||
|
</VButton>
|
||||||
|
</VSpace>
|
||||||
|
</template>
|
||||||
|
</VEmpty>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<div v-if="viewType === 'grid'">
|
||||||
<div
|
<div
|
||||||
:class="{
|
class="mt-2 grid grid-cols-3 gap-x-2 gap-y-3 sm:grid-cols-3 md:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-12"
|
||||||
'bg-gray-100': checkAll,
|
role="list"
|
||||||
}"
|
|
||||||
class="relative block cursor-pointer px-4 py-3 transition-all hover:bg-gray-50"
|
|
||||||
>
|
>
|
||||||
<div
|
<VCard
|
||||||
v-show="checkAll"
|
v-for="(attachment, index) in attachments.items"
|
||||||
class="absolute inset-y-0 left-0 w-0.5 bg-primary"
|
:key="index"
|
||||||
></div>
|
:body-class="['!p-0']"
|
||||||
<div class="relative flex flex-row items-center">
|
:class="{
|
||||||
<div class="mr-4 hidden items-center sm:flex">
|
'ring-1 ring-primary': isChecked(attachment),
|
||||||
<input
|
'ring-1 ring-red-600':
|
||||||
v-model="checkAll"
|
attachment.metadata.deletionTimestamp,
|
||||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
|
}"
|
||||||
type="checkbox"
|
class="hover:shadow"
|
||||||
/>
|
@click="handleClickItem(attachment)"
|
||||||
</div>
|
>
|
||||||
<div class="flex-1">
|
<div class="group relative bg-white">
|
||||||
<div class="flex flex-col sm:flex-row">
|
|
||||||
<span
|
|
||||||
class="mr-0 truncate text-sm font-medium text-gray-900 sm:mr-2"
|
|
||||||
>
|
|
||||||
{{ attachment.name }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="mt-1 flex">
|
|
||||||
<VSpace>
|
|
||||||
<span class="text-xs text-gray-500">image/png</span>
|
|
||||||
<span class="text-xs text-gray-500">1.2 MB</span>
|
|
||||||
</VSpace>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex">
|
|
||||||
<div
|
<div
|
||||||
class="inline-flex flex-col flex-col-reverse items-end gap-4 sm:flex-row sm:items-center sm:gap-6"
|
class="aspect-w-10 aspect-h-8 block h-full w-full cursor-pointer overflow-hidden bg-gray-100"
|
||||||
>
|
>
|
||||||
<img
|
<LazyImage
|
||||||
class="hidden h-6 w-6 rounded-full ring-2 ring-white sm:inline-block"
|
v-if="isImage(attachment.spec.mediaType)"
|
||||||
src="https://ryanc.cc/avatar"
|
:key="attachment.metadata.name"
|
||||||
|
:alt="attachment.spec.displayName"
|
||||||
|
:src="attachment.status?.permalink"
|
||||||
|
classes="pointer-events-none object-cover group-hover:opacity-75"
|
||||||
|
>
|
||||||
|
<template #loading>
|
||||||
|
<div
|
||||||
|
class="flex h-full items-center justify-center object-cover"
|
||||||
|
>
|
||||||
|
<span class="text-xs text-gray-400">加载中...</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #error>
|
||||||
|
<div
|
||||||
|
class="flex h-full items-center justify-center object-cover"
|
||||||
|
>
|
||||||
|
<span class="text-xs text-red-400">加载异常</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</LazyImage>
|
||||||
|
<AttachmentFileTypeIcon
|
||||||
|
v-else
|
||||||
|
:file-name="attachment.spec.displayName"
|
||||||
/>
|
/>
|
||||||
<time class="text-sm text-gray-500" datetime="2020-01-07">
|
</div>
|
||||||
2020-01-07
|
|
||||||
</time>
|
<p
|
||||||
<span class="cursor-pointer">
|
v-tooltip="attachment.spec.displayName"
|
||||||
<IconSettings @click.stop="detailVisible = true" />
|
class="block cursor-pointer truncate px-2 py-1 text-center text-xs font-medium text-gray-700"
|
||||||
</span>
|
>
|
||||||
|
{{ attachment.spec.displayName }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="attachment.metadata.deletionTimestamp"
|
||||||
|
class="absolute top-1 right-1 text-xs text-red-300"
|
||||||
|
>
|
||||||
|
删除中...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="!attachment.metadata.deletionTimestamp"
|
||||||
|
:class="{ '!flex': selectedAttachments.has(attachment) }"
|
||||||
|
class="absolute top-0 left-0 hidden h-1/3 w-full cursor-pointer justify-end bg-gradient-to-b from-gray-300 to-transparent ease-in-out group-hover:flex"
|
||||||
|
>
|
||||||
|
<IconCheckboxFill
|
||||||
|
:class="{
|
||||||
|
'!text-primary': selectedAttachments.has(attachment),
|
||||||
|
}"
|
||||||
|
class="mt-1 mr-1 h-6 w-6 cursor-pointer text-white transition-all hover:text-primary"
|
||||||
|
@click.stop="handleSelect(attachment)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</VCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul
|
||||||
|
v-if="viewType === 'list'"
|
||||||
|
class="box-border h-full w-full divide-y divide-gray-100"
|
||||||
|
role="list"
|
||||||
|
>
|
||||||
|
<li v-for="(attachment, index) in attachments.items" :key="index">
|
||||||
|
<div
|
||||||
|
:class="{
|
||||||
|
'bg-gray-100': isChecked(attachment),
|
||||||
|
}"
|
||||||
|
class="relative block cursor-pointer px-4 py-3 transition-all hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-show="isChecked(attachment)"
|
||||||
|
class="absolute inset-y-0 left-0 w-0.5 bg-primary"
|
||||||
|
></div>
|
||||||
|
<div class="relative flex flex-row items-center">
|
||||||
|
<div class="mr-4 hidden items-center sm:flex">
|
||||||
|
<input
|
||||||
|
:checked="selectedAttachments.has(attachment)"
|
||||||
|
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
|
||||||
|
type="checkbox"
|
||||||
|
@click="handleSelect(attachment)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mr-4">
|
||||||
|
<div
|
||||||
|
class="h-12 w-12 rounded border bg-white p-1 hover:shadow-sm"
|
||||||
|
>
|
||||||
|
<AttachmentFileTypeIcon
|
||||||
|
:display-ext="false"
|
||||||
|
:file-name="attachment.spec.displayName"
|
||||||
|
:width="8"
|
||||||
|
:height="8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex flex-col sm:flex-row">
|
||||||
|
<span
|
||||||
|
class="mr-0 truncate text-sm font-medium text-gray-900 sm:mr-2"
|
||||||
|
@click="handleClickItem(attachment)"
|
||||||
|
>
|
||||||
|
{{ attachment.spec.displayName }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-1 flex">
|
||||||
|
<VSpace>
|
||||||
|
<span class="text-xs text-gray-500">
|
||||||
|
{{ attachment.spec.mediaType }}
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-gray-500">
|
||||||
|
{{ prettyBytes(attachment.spec.size || 0) }}
|
||||||
|
</span>
|
||||||
|
</VSpace>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<div
|
||||||
|
class="inline-flex flex-col items-end gap-4 sm:flex-row sm:items-center sm:gap-6"
|
||||||
|
>
|
||||||
|
<span class="text-sm text-gray-500">
|
||||||
|
{{ getPolicyName(attachment.spec.policyRef?.name) }}
|
||||||
|
</span>
|
||||||
|
<RouterLink
|
||||||
|
:to="{
|
||||||
|
name: 'UserDetail',
|
||||||
|
params: { name: attachment.spec.uploadedBy?.name },
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span class="text-sm text-gray-500">
|
||||||
|
{{ attachment.spec.uploadedBy?.name }}
|
||||||
|
</span>
|
||||||
|
</RouterLink>
|
||||||
|
<FloatingTooltip
|
||||||
|
v-if="attachment.metadata.deletionTimestamp"
|
||||||
|
class="hidden items-center sm:flex"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="inline-flex h-1.5 w-1.5 rounded-full bg-red-600"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-red-600"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
<template #popper> 删除中</template>
|
||||||
|
</FloatingTooltip>
|
||||||
|
<time class="text-sm text-gray-500">
|
||||||
|
{{
|
||||||
|
formatDatetime(
|
||||||
|
attachment.metadata.creationTimestamp
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</time>
|
||||||
|
<span class="cursor-pointer">
|
||||||
|
<IconSettings
|
||||||
|
@click.stop="handleClickItem(attachment)"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
</ul>
|
||||||
</ul>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="bg-white sm:flex sm:items-center sm:justify-end">
|
<div class="bg-white sm:flex sm:items-center sm:justify-end">
|
||||||
<VPagination :page="1" :size="10" :total="20" />
|
<VPagination
|
||||||
|
:page="attachments.page"
|
||||||
|
:size="attachments.size"
|
||||||
|
:total="attachments.total"
|
||||||
|
@change="handlePaginationChange"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</VCard>
|
</VCard>
|
||||||
|
|
|
@ -1,99 +0,0 @@
|
||||||
<script lang="ts" setup>
|
|
||||||
import { VAlert, VButton, VModal, VSpace } from "@halo-dev/components";
|
|
||||||
|
|
||||||
withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
visible: boolean;
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
visible: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
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
|
|
||||||
:visible="visible"
|
|
||||||
:width="600"
|
|
||||||
title="阿里云 OSS 存储策略编辑"
|
|
||||||
@update:visible="onVisibleChange"
|
|
||||||
>
|
|
||||||
<VAlert class="mb-4" title="提示">
|
|
||||||
<template #description>
|
|
||||||
<p class="my-3 text-sm text-gray-600">
|
|
||||||
阿里云 OSS 产品文档:<a
|
|
||||||
href="https://help.aliyun.com/product/31815.html"
|
|
||||||
target="_blank"
|
|
||||||
>https://help.aliyun.com/product/31815.html</a
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
</VAlert>
|
|
||||||
<FormKit id="alioss-strategy-form" type="form">
|
|
||||||
<FormKit label="名称" type="text" validation="required"></FormKit>
|
|
||||||
<FormKit label="Bucket" type="text" validation="required"></FormKit>
|
|
||||||
<FormKit label="EndPoint" type="text" validation="required"></FormKit>
|
|
||||||
<FormKit label="Access Key" type="text" validation="required"></FormKit>
|
|
||||||
<FormKit
|
|
||||||
label="Access Secret"
|
|
||||||
type="text"
|
|
||||||
validation="required"
|
|
||||||
></FormKit>
|
|
||||||
<FormKit
|
|
||||||
label="上传目录"
|
|
||||||
placeholder="如不填写,则默认上传到根目录"
|
|
||||||
type="text"
|
|
||||||
></FormKit>
|
|
||||||
<FormKit
|
|
||||||
:options="[
|
|
||||||
{ label: 'HTTPS', value: 'https' },
|
|
||||||
{ label: 'HTTP', value: 'http' },
|
|
||||||
]"
|
|
||||||
label="绑定域名协议"
|
|
||||||
type="select"
|
|
||||||
></FormKit>
|
|
||||||
<FormKit
|
|
||||||
label="绑定域名"
|
|
||||||
placeholder="如不设置,那么将使用 Bucket + EndPoint 作为域名"
|
|
||||||
type="text"
|
|
||||||
></FormKit>
|
|
||||||
<FormKit
|
|
||||||
:options="[
|
|
||||||
{ label: '服务端上传', value: 'server' },
|
|
||||||
{ label: '客户端上传', value: 'client' },
|
|
||||||
]"
|
|
||||||
label="上传方式"
|
|
||||||
type="select"
|
|
||||||
></FormKit>
|
|
||||||
<FormKit
|
|
||||||
help="使用半角逗号分隔"
|
|
||||||
label="允许上传的文件类型"
|
|
||||||
type="textarea"
|
|
||||||
value="jpg,png,gif"
|
|
||||||
></FormKit>
|
|
||||||
</FormKit>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<VSpace>
|
|
||||||
<VButton
|
|
||||||
type="secondary"
|
|
||||||
@click="$formkit.submit('alioss-strategy-form')"
|
|
||||||
>
|
|
||||||
保存 ⌘ + ↵
|
|
||||||
</VButton>
|
|
||||||
<VButton @click="onVisibleChange(false)">取消 Esc</VButton>
|
|
||||||
</VSpace>
|
|
||||||
</template>
|
|
||||||
</VModal>
|
|
||||||
</template>
|
|
|
@ -1,19 +1,24 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {
|
import { VButton, VModal, VSpace, VTag } from "@halo-dev/components";
|
||||||
IconArrowLeft,
|
import LazyImage from "@/components/image/LazyImage.vue";
|
||||||
IconArrowRight,
|
import type { Attachment, Policy } from "@halo-dev/api-client";
|
||||||
VButton,
|
import prettyBytes from "pretty-bytes";
|
||||||
VModal,
|
import { ref, watch, watchEffect } from "vue";
|
||||||
VSpace,
|
import { apiClient } from "@halo-dev/admin-shared";
|
||||||
VTag,
|
import { isImage } from "@/utils/image";
|
||||||
} from "@halo-dev/components";
|
import { formatDatetime } from "@/utils/date";
|
||||||
|
import { useFetchAttachmentGroup } from "../composables/use-attachment-group";
|
||||||
|
|
||||||
withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
attachment: Attachment | null;
|
||||||
|
mountToBody?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
visible: false,
|
visible: false,
|
||||||
|
attachment: null,
|
||||||
|
mountToBody: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -22,6 +27,39 @@ const emit = defineEmits<{
|
||||||
(event: "close"): void;
|
(event: "close"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const { groups, handleFetchGroups } = useFetchAttachmentGroup();
|
||||||
|
|
||||||
|
const policy = ref<Policy>();
|
||||||
|
const onlyPreview = ref(false);
|
||||||
|
|
||||||
|
watchEffect(async () => {
|
||||||
|
if (props.attachment) {
|
||||||
|
const { policyRef } = props.attachment.spec;
|
||||||
|
if (!policyRef) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { data } =
|
||||||
|
await apiClient.extension.storage.policy.getstorageHaloRunV1alpha1Policy(
|
||||||
|
policyRef.name
|
||||||
|
);
|
||||||
|
policy.value = data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
handleFetchGroups();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const getGroupName = (name: string | undefined) => {
|
||||||
|
const group = groups.value.find((group) => group.metadata.name === name);
|
||||||
|
return group?.spec.displayName || name;
|
||||||
|
};
|
||||||
|
|
||||||
const onVisibleChange = (visible: boolean) => {
|
const onVisibleChange = (visible: boolean) => {
|
||||||
emit("update:visible", visible);
|
emit("update:visible", visible);
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
|
@ -31,149 +69,159 @@ const onVisibleChange = (visible: boolean) => {
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<VModal
|
<VModal
|
||||||
|
:title="`附件:${attachment?.spec.displayName || ''}`"
|
||||||
:visible="visible"
|
:visible="visible"
|
||||||
:width="1000"
|
:width="1000"
|
||||||
title="attachment-0"
|
:mount-to-body="mountToBody"
|
||||||
|
height="calc(100vh - 20px)"
|
||||||
@update:visible="onVisibleChange"
|
@update:visible="onVisibleChange"
|
||||||
>
|
>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<div class="modal-header-action">
|
<slot name="actions"></slot>
|
||||||
<IconArrowLeft />
|
|
||||||
</div>
|
|
||||||
<div class="modal-header-action">
|
|
||||||
<IconArrowRight />
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<div class="overflow-hidden bg-white">
|
<div class="overflow-hidden bg-white">
|
||||||
<div>
|
<div
|
||||||
<dl>
|
v-if="onlyPreview && isImage(attachment?.spec.mediaType)"
|
||||||
<div
|
class="flex justify-center"
|
||||||
class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"
|
>
|
||||||
>
|
<img
|
||||||
<dt class="text-sm font-medium text-gray-900">原始内容</dt>
|
v-tooltip.bottom="`点击退出预览`"
|
||||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
:alt="attachment?.spec.displayName"
|
||||||
<img
|
:src="attachment?.status?.permalink"
|
||||||
class="w-full rounded sm:w-1/2"
|
class="w-auto cursor-pointer rounded"
|
||||||
src="https://picsum.photos/1000/700?random=1"
|
@click="onlyPreview = !onlyPreview"
|
||||||
/>
|
/>
|
||||||
</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>
|
|
||||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
|
||||||
阿里云/bucket/blog-attachments
|
|
||||||
</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>
|
|
||||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
|
||||||
Photos
|
|
||||||
</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>
|
|
||||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
|
||||||
attachment-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>
|
|
||||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
|
||||||
image/jpeg
|
|
||||||
</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>
|
|
||||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
|
||||||
1.2 MB
|
|
||||||
</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>
|
|
||||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
|
||||||
Ryan Wang
|
|
||||||
</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>
|
|
||||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
|
||||||
2020-01-01 12:00:00
|
|
||||||
</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>
|
|
||||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
|
||||||
https://picsum.photos/1000/700?random=1
|
|
||||||
</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>
|
|
||||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
|
||||||
<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>
|
</div>
|
||||||
|
<dl v-else>
|
||||||
|
<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>
|
||||||
|
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||||
|
<LazyImage
|
||||||
|
v-if="isImage(attachment?.spec.mediaType)"
|
||||||
|
:alt="attachment?.spec.displayName"
|
||||||
|
:src="attachment?.status?.permalink"
|
||||||
|
class="max-w-full cursor-pointer rounded sm:max-w-[50%]"
|
||||||
|
@click="onlyPreview = !onlyPreview"
|
||||||
|
>
|
||||||
|
<template #loading>
|
||||||
|
<span class="text-gray-400">加载中...</span>
|
||||||
|
</template>
|
||||||
|
<template #error>
|
||||||
|
<span class="text-red-400">加载异常</span>
|
||||||
|
</template>
|
||||||
|
</LazyImage>
|
||||||
|
<span v-else> 此文件不支持预览 </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>
|
||||||
|
<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>
|
||||||
|
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||||
|
{{ getGroupName(attachment?.spec.groupRef?.name) || "未分组" }}
|
||||||
|
</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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||||
|
{{ attachment?.spec.uploadedBy?.name }}
|
||||||
|
</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>
|
||||||
|
<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>
|
||||||
|
<dd
|
||||||
|
class="mt-1 text-sm text-gray-900 hover:text-blue-600 sm:col-span-2 sm:mt-0"
|
||||||
|
>
|
||||||
|
<a target="_blank" :href="attachment?.status?.permalink">
|
||||||
|
{{ attachment?.status?.permalink }}
|
||||||
|
</a>
|
||||||
|
</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>
|
||||||
|
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||||
|
// TODO
|
||||||
|
<ul v-if="false" 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>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<VButton type="default" @click="onVisibleChange(false)">关闭</VButton>
|
<VSpace>
|
||||||
|
<VButton type="default" @click="onVisibleChange(false)"
|
||||||
|
>关闭 Esc</VButton
|
||||||
|
>
|
||||||
|
<slot name="footer" />
|
||||||
|
</VSpace>
|
||||||
</template>
|
</template>
|
||||||
</VModal>
|
</VModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import VscodeIconsDefaultFile from "~icons/vscode-icons/default-file";
|
||||||
|
import VscodeIconsFileTypeImage from "~icons/vscode-icons/file-type-image";
|
||||||
|
import VscodeIconsFileTypePhotoshop from "~icons/vscode-icons/file-type-photoshop";
|
||||||
|
import VscodeIconsFileTypeAi from "~icons/vscode-icons/file-type-ai";
|
||||||
|
import VscodeIconsFileTypeWord from "~icons/vscode-icons/file-type-word";
|
||||||
|
import VscodeIconsFileTypePowerpoint from "~icons/vscode-icons/file-type-powerpoint";
|
||||||
|
import VscodeIconsFileTypeExcel from "~icons/vscode-icons/file-type-excel";
|
||||||
|
import VscodeIconsFileTypeText from "~icons/vscode-icons/file-type-text";
|
||||||
|
import VscodeIconsFileTypeJson from "~icons/vscode-icons/file-type-json";
|
||||||
|
import VscodeIconsFileTypeHtml from "~icons/vscode-icons/file-type-html";
|
||||||
|
import VscodeIconsFileTypeYaml from "~icons/vscode-icons/file-type-yaml";
|
||||||
|
import VscodeIconsFileTypeXml from "~icons/vscode-icons/file-type-xml";
|
||||||
|
import VscodeIconsFileTypeJava from "~icons/vscode-icons/file-type-java";
|
||||||
|
import VscodeIconsFileTypeJar from "~icons/vscode-icons/file-type-jar";
|
||||||
|
import VscodeIconsFileTypeClass from "~icons/vscode-icons/file-type-class";
|
||||||
|
import VscodeIconsFileTypeJsOfficial from "~icons/vscode-icons/file-type-js-official";
|
||||||
|
import VscodeIconsFileTypeTypescriptOfficial from "~icons/vscode-icons/file-type-typescript-official";
|
||||||
|
import VscodeIconsFileTypeVue from "~icons/vscode-icons/file-type-vue";
|
||||||
|
import VscodeIconsFileTypeGo from "~icons/vscode-icons/file-type-go";
|
||||||
|
import VscodeIconsFileTypeC from "~icons/vscode-icons/file-type-c";
|
||||||
|
import VscodeIconsFileTypeCpp from "~icons/vscode-icons/file-type-cpp";
|
||||||
|
import VscodeIconsFileTypeAstro from "~icons/vscode-icons/file-type-astro";
|
||||||
|
import VscodeIconsFileTypeBat from "~icons/vscode-icons/file-type-bat";
|
||||||
|
import VscodeIconsFileTypeCss from "~icons/vscode-icons/file-type-css";
|
||||||
|
import VscodeIconsFileTypeDb from "~icons/vscode-icons/file-type-db";
|
||||||
|
import VscodeIconsFileTypeGradle from "~icons/vscode-icons/file-type-gradle";
|
||||||
|
import VscodeIconsFileTypeMarkdown from "~icons/vscode-icons/file-type-markdown";
|
||||||
|
import VscodeIconsFileTypePython from "~icons/vscode-icons/file-type-python";
|
||||||
|
import VscodeIconsFileTypeShell from "~icons/vscode-icons/file-type-shell";
|
||||||
|
import VscodeIconsFileTypePhp3 from "~icons/vscode-icons/file-type-php3";
|
||||||
|
import { extname } from "path-browserify";
|
||||||
|
import { computed, markRaw } from "vue";
|
||||||
|
|
||||||
|
const FileTypeIconsMap = {
|
||||||
|
// image
|
||||||
|
".jpg": markRaw(VscodeIconsFileTypeImage),
|
||||||
|
".png": markRaw(VscodeIconsFileTypeImage),
|
||||||
|
".gif": markRaw(VscodeIconsFileTypeImage),
|
||||||
|
".webp": markRaw(VscodeIconsFileTypeImage),
|
||||||
|
".svg": markRaw(VscodeIconsFileTypeImage),
|
||||||
|
|
||||||
|
// documnet
|
||||||
|
".docx": markRaw(VscodeIconsFileTypeWord),
|
||||||
|
".pptx": markRaw(VscodeIconsFileTypePowerpoint),
|
||||||
|
".xlsx": markRaw(VscodeIconsFileTypeExcel),
|
||||||
|
".psd": markRaw(VscodeIconsFileTypePhotoshop),
|
||||||
|
".ai": markRaw(VscodeIconsFileTypeAi),
|
||||||
|
".txt": markRaw(VscodeIconsFileTypeText),
|
||||||
|
|
||||||
|
// programming languages or frameworks
|
||||||
|
".json": markRaw(VscodeIconsFileTypeJson),
|
||||||
|
".html": markRaw(VscodeIconsFileTypeHtml),
|
||||||
|
".yaml": markRaw(VscodeIconsFileTypeYaml),
|
||||||
|
".xml": markRaw(VscodeIconsFileTypeXml),
|
||||||
|
".java": markRaw(VscodeIconsFileTypeJava),
|
||||||
|
".jar": markRaw(VscodeIconsFileTypeJar),
|
||||||
|
".class": markRaw(VscodeIconsFileTypeClass),
|
||||||
|
".js": markRaw(VscodeIconsFileTypeJsOfficial),
|
||||||
|
".ts": markRaw(VscodeIconsFileTypeTypescriptOfficial),
|
||||||
|
".vue": markRaw(VscodeIconsFileTypeVue),
|
||||||
|
".go": markRaw(VscodeIconsFileTypeGo),
|
||||||
|
".c": markRaw(VscodeIconsFileTypeC),
|
||||||
|
".cpp": markRaw(VscodeIconsFileTypeCpp),
|
||||||
|
".astro": markRaw(VscodeIconsFileTypeAstro),
|
||||||
|
".bat": markRaw(VscodeIconsFileTypeBat),
|
||||||
|
".css": markRaw(VscodeIconsFileTypeCss),
|
||||||
|
".db": markRaw(VscodeIconsFileTypeDb),
|
||||||
|
".gradle": markRaw(VscodeIconsFileTypeGradle),
|
||||||
|
".md": markRaw(VscodeIconsFileTypeMarkdown),
|
||||||
|
".py": markRaw(VscodeIconsFileTypePython),
|
||||||
|
".sh": markRaw(VscodeIconsFileTypeShell),
|
||||||
|
".php": markRaw(VscodeIconsFileTypePhp3),
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
fileName: string | undefined;
|
||||||
|
displayExt?: boolean;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
fileName: undefined,
|
||||||
|
displayExt: true,
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const getExtname = computed(() => {
|
||||||
|
const ext = extname(props.fileName);
|
||||||
|
if (ext) return ext.toLowerCase();
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getIcon = computed(() => {
|
||||||
|
const icon = FileTypeIconsMap[getExtname.value];
|
||||||
|
if (icon) return icon;
|
||||||
|
return markRaw(VscodeIconsDefaultFile);
|
||||||
|
});
|
||||||
|
|
||||||
|
const iconClass = computed(() => {
|
||||||
|
return [`w-${props.width}`, `h-${props.height}`];
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="flex h-full w-full flex-col items-center justify-center gap-1">
|
||||||
|
<component :is="getIcon" :class="iconClass" />
|
||||||
|
<span
|
||||||
|
v-if="getExtname && displayExt"
|
||||||
|
class="font-sans text-xs text-gray-500"
|
||||||
|
>
|
||||||
|
{{ getExtname }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -1,10 +1,17 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { VButton, VModal, VSpace } from "@halo-dev/components";
|
import { VButton, VModal, VSpace } from "@halo-dev/components";
|
||||||
|
import type { Group } from "@halo-dev/api-client";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
import { computed, ref, watch, watchEffect } from "vue";
|
||||||
|
import cloneDeep from "lodash.clonedeep";
|
||||||
|
import { apiClient } from "@halo-dev/admin-shared";
|
||||||
|
import { reset, submitForm } from "@formkit/core";
|
||||||
|
import { useMagicKeys } from "@vueuse/core";
|
||||||
|
|
||||||
withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
group: object | null;
|
group: Group | null;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
visible: false,
|
visible: false,
|
||||||
|
@ -17,26 +24,109 @@ const emit = defineEmits<{
|
||||||
(event: "close"): void;
|
(event: "close"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const initialFormState: Group = {
|
||||||
|
spec: {
|
||||||
|
displayName: "",
|
||||||
|
},
|
||||||
|
apiVersion: "storage.halo.run/v1alpha1",
|
||||||
|
kind: "Group",
|
||||||
|
metadata: {
|
||||||
|
name: uuid(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const formState = ref<Group>(cloneDeep(initialFormState));
|
||||||
|
const saving = ref(false);
|
||||||
|
|
||||||
|
const isUpdateMode = computed(() => {
|
||||||
|
return !!formState.value.metadata.creationTimestamp;
|
||||||
|
});
|
||||||
|
|
||||||
|
const modalTitle = computed(() => {
|
||||||
|
return isUpdateMode.value ? "编辑附件分组" : "新增附件分组";
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
try {
|
||||||
|
saving.value = true;
|
||||||
|
if (isUpdateMode.value) {
|
||||||
|
await apiClient.extension.storage.group.updatestorageHaloRunV1alpha1Group(
|
||||||
|
formState.value.metadata.name,
|
||||||
|
formState.value
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await apiClient.extension.storage.group.createstorageHaloRunV1alpha1Group(
|
||||||
|
formState.value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
onVisibleChange(false);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to save attachment group", e);
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onVisibleChange = (visible: boolean) => {
|
const onVisibleChange = (visible: boolean) => {
|
||||||
emit("update:visible", visible);
|
emit("update:visible", visible);
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
emit("close");
|
emit("close");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleResetForm = () => {
|
||||||
|
formState.value = cloneDeep(initialFormState);
|
||||||
|
formState.value.metadata.name = uuid();
|
||||||
|
reset("attachment-group-form");
|
||||||
|
};
|
||||||
|
|
||||||
|
const { Command_Enter } = useMagicKeys();
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (Command_Enter.value && props.visible) {
|
||||||
|
submitForm("attachment-group-form");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
(visible) => {
|
||||||
|
if (!visible) {
|
||||||
|
handleResetForm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.group,
|
||||||
|
(group) => {
|
||||||
|
if (group) {
|
||||||
|
formState.value = cloneDeep(group);
|
||||||
|
} else {
|
||||||
|
handleResetForm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<VModal
|
<VModal
|
||||||
|
:title="modalTitle"
|
||||||
:visible="visible"
|
:visible="visible"
|
||||||
:width="500"
|
:width="500"
|
||||||
title="附件分组"
|
|
||||||
@update:visible="onVisibleChange"
|
@update:visible="onVisibleChange"
|
||||||
>
|
>
|
||||||
<FormKit id="attachment-group-form" type="form">
|
<FormKit id="attachment-group-form" type="form" @submit="handleSave">
|
||||||
<FormKit label="名称" type="text" validation="required"></FormKit>
|
<FormKit
|
||||||
|
v-model="formState.spec.displayName"
|
||||||
|
label="名称"
|
||||||
|
type="text"
|
||||||
|
validation="required"
|
||||||
|
></FormKit>
|
||||||
</FormKit>
|
</FormKit>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<VSpace>
|
<VSpace>
|
||||||
<VButton
|
<VButton
|
||||||
|
:loading="saving"
|
||||||
type="secondary"
|
type="secondary"
|
||||||
@click="$formkit.submit('attachment-group-form')"
|
@click="$formkit.submit('attachment-group-form')"
|
||||||
>
|
>
|
||||||
|
|
|
@ -0,0 +1,165 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
// core libs
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { IconAddCircle, IconMore, VButton, VSpace } from "@halo-dev/components";
|
||||||
|
import AttachmentGroupEditingModal from "./AttachmentGroupEditingModal.vue";
|
||||||
|
|
||||||
|
// types
|
||||||
|
import type { Group } from "@halo-dev/api-client";
|
||||||
|
|
||||||
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
|
import { useFetchAttachmentGroup } from "../composables/use-attachment-group";
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
selectedGroup: Group | undefined;
|
||||||
|
readonly?: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
selectedGroup: undefined,
|
||||||
|
readonly: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: "update:selectedGroup", group: Group): void;
|
||||||
|
(event: "select", group: Group): void;
|
||||||
|
(event: "update"): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const defaultGroups: Group[] = [
|
||||||
|
{
|
||||||
|
spec: {
|
||||||
|
displayName: "全部",
|
||||||
|
},
|
||||||
|
apiVersion: "",
|
||||||
|
kind: "",
|
||||||
|
metadata: {
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
spec: {
|
||||||
|
displayName: "未分组",
|
||||||
|
},
|
||||||
|
apiVersion: "",
|
||||||
|
kind: "",
|
||||||
|
metadata: {
|
||||||
|
name: "none",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const { groups, handleFetchGroups } = useFetchAttachmentGroup();
|
||||||
|
|
||||||
|
const groupToUpdate = ref<Group | null>(null);
|
||||||
|
const loading = ref<boolean>(false);
|
||||||
|
const editingModal = ref(false);
|
||||||
|
|
||||||
|
const routeQuery = useRouteQuery("group");
|
||||||
|
|
||||||
|
const handleSelectGroup = (group: Group) => {
|
||||||
|
emit("update:selectedGroup", group);
|
||||||
|
emit("select", group);
|
||||||
|
|
||||||
|
if (!props.readonly) {
|
||||||
|
routeQuery.value = group.metadata.name;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenEditingModal = (group: Group) => {
|
||||||
|
groupToUpdate.value = group;
|
||||||
|
editingModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEditingModalClose = () => {
|
||||||
|
emit("update");
|
||||||
|
handleFetchGroups();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await handleFetchGroups();
|
||||||
|
|
||||||
|
if (routeQuery.value && !props.readonly) {
|
||||||
|
const allGroups = [...defaultGroups, ...groups.value];
|
||||||
|
const group = allGroups.find(
|
||||||
|
(group) => group.metadata.name === routeQuery.value
|
||||||
|
);
|
||||||
|
if (group) {
|
||||||
|
handleSelectGroup(group);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSelectGroup(defaultGroups[0]);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<AttachmentGroupEditingModal
|
||||||
|
v-model:visible="editingModal"
|
||||||
|
:group="groupToUpdate"
|
||||||
|
@close="onEditingModalClose"
|
||||||
|
/>
|
||||||
|
<div class="mb-5 grid grid-cols-2 gap-x-2 gap-y-3 sm:grid-cols-6">
|
||||||
|
<div
|
||||||
|
v-for="(defaultGroup, index) in defaultGroups"
|
||||||
|
:key="index"
|
||||||
|
:class="{
|
||||||
|
'!bg-gray-200 !text-gray-900':
|
||||||
|
defaultGroup.metadata.name === selectedGroup?.metadata.name,
|
||||||
|
}"
|
||||||
|
class="flex 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"
|
||||||
|
@click="handleSelectGroup(defaultGroup)"
|
||||||
|
>
|
||||||
|
<div class="flex flex-1 items-center">
|
||||||
|
<span class="text-sm">{{ defaultGroup.spec.displayName }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="(group, index) in groups"
|
||||||
|
:key="index"
|
||||||
|
:class="{
|
||||||
|
'!bg-gray-200 !text-gray-900':
|
||||||
|
group.metadata.name === selectedGroup?.metadata.name,
|
||||||
|
}"
|
||||||
|
class="flex 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"
|
||||||
|
@click="handleSelectGroup(group)"
|
||||||
|
>
|
||||||
|
<div class="flex flex-1 items-center truncate">
|
||||||
|
<span class="truncate text-sm">
|
||||||
|
{{ group.spec.displayName }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<FloatingDropdown v-if="!readonly">
|
||||||
|
<IconMore />
|
||||||
|
<template #popper>
|
||||||
|
<div class="w-48 p-2">
|
||||||
|
<VSpace class="w-full" direction="column">
|
||||||
|
<VButton
|
||||||
|
v-close-popper
|
||||||
|
block
|
||||||
|
type="secondary"
|
||||||
|
@click="handleOpenEditingModal(group)"
|
||||||
|
>
|
||||||
|
重命名
|
||||||
|
</VButton>
|
||||||
|
<VButton v-close-popper block type="danger"> 删除</VButton>
|
||||||
|
</VSpace>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</FloatingDropdown>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="!loading && !readonly"
|
||||||
|
class="flex 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"
|
||||||
|
@click="editingModal = true"
|
||||||
|
>
|
||||||
|
<div class="flex flex-1 items-center truncate">
|
||||||
|
<span class="truncate text-sm">添加分组</span>
|
||||||
|
</div>
|
||||||
|
<IconAddCircle />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -1,55 +0,0 @@
|
||||||
<script lang="ts" setup>
|
|
||||||
import { VButton, VModal, VSpace } from "@halo-dev/components";
|
|
||||||
|
|
||||||
withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
visible: boolean;
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
visible: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
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
|
|
||||||
:visible="visible"
|
|
||||||
:width="600"
|
|
||||||
title="本地存储策略编辑"
|
|
||||||
@update:visible="onVisibleChange"
|
|
||||||
>
|
|
||||||
<FormKit id="local-strategy-form" type="form">
|
|
||||||
<FormKit label="名称" type="text" validation="required"></FormKit>
|
|
||||||
<FormKit label="存储位置" type="text" validation="required"></FormKit>
|
|
||||||
<FormKit
|
|
||||||
help="使用半角逗号分隔"
|
|
||||||
label="允许上传的文件类型"
|
|
||||||
type="textarea"
|
|
||||||
value="jpg,png,gif"
|
|
||||||
></FormKit>
|
|
||||||
</FormKit>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<VSpace>
|
|
||||||
<VButton
|
|
||||||
type="secondary"
|
|
||||||
@click="$formkit.submit('local-strategy-form')"
|
|
||||||
>
|
|
||||||
保存 ⌘ + ↵
|
|
||||||
</VButton>
|
|
||||||
<VButton @click="onVisibleChange(false)">取消 Esc</VButton>
|
|
||||||
</VSpace>
|
|
||||||
</template>
|
|
||||||
</VModal>
|
|
||||||
</template>
|
|
|
@ -0,0 +1,233 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {
|
||||||
|
IconAddCircle,
|
||||||
|
IconMore,
|
||||||
|
VButton,
|
||||||
|
VModal,
|
||||||
|
VSpace,
|
||||||
|
VEmpty,
|
||||||
|
} from "@halo-dev/components";
|
||||||
|
import AttachmentPolicyEditingModal from "./AttachmentPolicyEditingModal.vue";
|
||||||
|
import { ref, watch } from "vue";
|
||||||
|
import type { Policy, PolicyTemplate } from "@halo-dev/api-client";
|
||||||
|
import { apiClient } from "@halo-dev/admin-shared";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
import { formatDatetime } from "@/utils/date";
|
||||||
|
import { useFetchAttachmentPolicy } from "../composables/use-attachment-policy";
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
visible: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
visible: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: "update:visible", visible: boolean): void;
|
||||||
|
(event: "close"): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { policies, loading, handleFetchPolicies } = useFetchAttachmentPolicy();
|
||||||
|
|
||||||
|
const selectedPolicy = ref<Policy | null>(null);
|
||||||
|
const policyTemplates = ref<PolicyTemplate[]>([] as PolicyTemplate[]);
|
||||||
|
|
||||||
|
const policyEditingModal = ref(false);
|
||||||
|
|
||||||
|
const handleFetchPolicyTemplates = async () => {
|
||||||
|
try {
|
||||||
|
const { data } =
|
||||||
|
await apiClient.extension.storage.policyTemplate.liststorageHaloRunV1alpha1PolicyTemplate();
|
||||||
|
policyTemplates.value = data.items;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to fetch attachment policy templates", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function onVisibleChange(visible: boolean) {
|
||||||
|
emit("update:visible", visible);
|
||||||
|
if (!visible) {
|
||||||
|
emit("close");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOpenEditingModal = (policy: Policy) => {
|
||||||
|
selectedPolicy.value = policy;
|
||||||
|
policyEditingModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenCreateNewPolicyModal = (policyTemplate: PolicyTemplate) => {
|
||||||
|
selectedPolicy.value = {
|
||||||
|
spec: {
|
||||||
|
displayName: "",
|
||||||
|
templateRef: {
|
||||||
|
name: policyTemplate.metadata.name,
|
||||||
|
},
|
||||||
|
configMapRef: {
|
||||||
|
name: uuid(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
apiVersion: "storage.halo.run/v1alpha1",
|
||||||
|
kind: "Policy",
|
||||||
|
metadata: {
|
||||||
|
name: uuid(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
policyEditingModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEditingModalClose = () => {
|
||||||
|
selectedPolicy.value = null;
|
||||||
|
handleFetchPolicies();
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
handleFetchPolicyTemplates();
|
||||||
|
handleFetchPolicies();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<VModal
|
||||||
|
:visible="visible"
|
||||||
|
:width="750"
|
||||||
|
title="存储策略"
|
||||||
|
:body-class="['!p-0']"
|
||||||
|
@update:visible="onVisibleChange"
|
||||||
|
>
|
||||||
|
<template #actions>
|
||||||
|
<FloatingDropdown>
|
||||||
|
<div v-tooltip="`添加存储策略`" class="modal-header-action">
|
||||||
|
<IconAddCircle />
|
||||||
|
</div>
|
||||||
|
<template #popper>
|
||||||
|
<div class="w-72 p-4">
|
||||||
|
<ul class="space-y-1">
|
||||||
|
<li
|
||||||
|
v-for="(policyTemplate, index) in policyTemplates"
|
||||||
|
:key="index"
|
||||||
|
v-close-popper
|
||||||
|
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
||||||
|
@click="handleOpenCreateNewPolicyModal(policyTemplate)"
|
||||||
|
>
|
||||||
|
<span class="truncate">
|
||||||
|
{{ policyTemplate.spec?.displayName }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</FloatingDropdown>
|
||||||
|
</template>
|
||||||
|
<VEmpty
|
||||||
|
v-if="!policies.length && !loading"
|
||||||
|
message="当前没有可用的存储策略,你可以尝试刷新或者新建策略"
|
||||||
|
title="当前没有可用的存储策略"
|
||||||
|
>
|
||||||
|
<template #actions>
|
||||||
|
<VSpace>
|
||||||
|
<VButton @click="handleFetchPolicies">刷新</VButton>
|
||||||
|
<FloatingDropdown>
|
||||||
|
<VButton type="secondary">
|
||||||
|
<template #icon>
|
||||||
|
<IconAddCircle class="h-full w-full" />
|
||||||
|
</template>
|
||||||
|
新建策略
|
||||||
|
</VButton>
|
||||||
|
<template #popper>
|
||||||
|
<div class="w-72 p-4">
|
||||||
|
<ul class="space-y-1">
|
||||||
|
<li
|
||||||
|
v-for="(policyTemplate, index) in policyTemplates"
|
||||||
|
:key="index"
|
||||||
|
v-close-popper
|
||||||
|
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
||||||
|
@click="handleOpenCreateNewPolicyModal(policyTemplate)"
|
||||||
|
>
|
||||||
|
<span class="truncate">
|
||||||
|
{{ policyTemplate.spec?.displayName }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</FloatingDropdown>
|
||||||
|
</VSpace>
|
||||||
|
</template>
|
||||||
|
</VEmpty>
|
||||||
|
<ul
|
||||||
|
v-else
|
||||||
|
class="box-border h-full w-full divide-y divide-gray-100"
|
||||||
|
role="list"
|
||||||
|
>
|
||||||
|
<li v-for="(policy, index) in policies" :key="index">
|
||||||
|
<div
|
||||||
|
class="relative block cursor-pointer px-4 py-3 transition-all hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<div class="relative flex flex-row items-center">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex flex-col sm:flex-row">
|
||||||
|
<span
|
||||||
|
class="mr-0 truncate text-sm font-medium text-gray-900 sm:mr-2"
|
||||||
|
>
|
||||||
|
{{ policy.spec.displayName }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-1 flex">
|
||||||
|
<span class="text-xs text-gray-500">
|
||||||
|
{{ policy.spec.templateRef?.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<div
|
||||||
|
class="inline-flex flex-col items-end gap-4 sm:flex-row sm:items-center sm:gap-6"
|
||||||
|
>
|
||||||
|
<time class="text-sm text-gray-500">
|
||||||
|
{{ formatDatetime(policy.metadata.creationTimestamp) }}
|
||||||
|
</time>
|
||||||
|
<span class="cursor-pointer">
|
||||||
|
<FloatingDropdown>
|
||||||
|
<IconMore />
|
||||||
|
<template #popper>
|
||||||
|
<div class="w-48 p-2">
|
||||||
|
<VSpace class="w-full" direction="column">
|
||||||
|
<VButton
|
||||||
|
v-close-popper
|
||||||
|
block
|
||||||
|
type="secondary"
|
||||||
|
@click="handleOpenEditingModal(policy)"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</VButton>
|
||||||
|
<VButton v-close-popper block type="danger">
|
||||||
|
删除
|
||||||
|
</VButton>
|
||||||
|
</VSpace>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</FloatingDropdown>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<template #footer>
|
||||||
|
<VButton @click="onVisibleChange(false)">关闭 Esc</VButton>
|
||||||
|
</template>
|
||||||
|
</VModal>
|
||||||
|
|
||||||
|
<AttachmentPolicyEditingModal
|
||||||
|
v-model:visible="policyEditingModal"
|
||||||
|
:policy="selectedPolicy"
|
||||||
|
@close="onEditingModalClose"
|
||||||
|
/>
|
||||||
|
</template>
|
|
@ -0,0 +1,213 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { VButton, VModal, VSpace } from "@halo-dev/components";
|
||||||
|
import type { Policy, PolicyTemplate } from "@halo-dev/api-client";
|
||||||
|
import cloneDeep from "lodash.clonedeep";
|
||||||
|
import { computed, ref, watch, watchEffect } from "vue";
|
||||||
|
import { apiClient, useSettingForm } from "@halo-dev/admin-shared";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
import { reset, submitForm } from "@formkit/core";
|
||||||
|
import { useMagicKeys } from "@vueuse/core";
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
visible: boolean;
|
||||||
|
policy: Policy | null;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
visible: false,
|
||||||
|
policy: null,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: "update:visible", visible: boolean): void;
|
||||||
|
(event: "close"): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const initialFormState: Policy = {
|
||||||
|
spec: {
|
||||||
|
displayName: "",
|
||||||
|
templateRef: {
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
configMapRef: {
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
apiVersion: "storage.halo.run/v1alpha1",
|
||||||
|
kind: "Policy",
|
||||||
|
metadata: {
|
||||||
|
name: uuid(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const formState = ref<Policy>(cloneDeep(initialFormState));
|
||||||
|
const policyTemplate = ref<PolicyTemplate | undefined>();
|
||||||
|
|
||||||
|
const settingName = computed(
|
||||||
|
() => policyTemplate.value?.spec?.settingRef?.name
|
||||||
|
);
|
||||||
|
const configMapName = computed(() => formState.value.spec.configMapRef?.name);
|
||||||
|
|
||||||
|
const {
|
||||||
|
settings,
|
||||||
|
configMapFormData,
|
||||||
|
saving,
|
||||||
|
handleFetchConfigMap,
|
||||||
|
handleFetchSettings,
|
||||||
|
handleSaveConfigMap,
|
||||||
|
handleReset: handleResetSettingForm,
|
||||||
|
} = useSettingForm(settingName, configMapName);
|
||||||
|
|
||||||
|
const formSchema = computed(() => {
|
||||||
|
if (!settings?.value?.spec) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return settings.value.spec.find((item) => item.group === "default")
|
||||||
|
?.formSchema;
|
||||||
|
});
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (settingName.value) {
|
||||||
|
handleFetchSettings();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (configMapName.value && settings.value) {
|
||||||
|
handleFetchConfigMap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const isUpdateMode = computed(() => {
|
||||||
|
return !!formState.value.metadata.creationTimestamp;
|
||||||
|
});
|
||||||
|
|
||||||
|
const modalTitle = computed(() => {
|
||||||
|
return isUpdateMode.value
|
||||||
|
? `编辑策略:${props.policy?.spec.displayName}`
|
||||||
|
: `新增策略:${policyTemplate.value?.spec?.displayName}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
try {
|
||||||
|
saving.value = true;
|
||||||
|
|
||||||
|
await handleSaveConfigMap();
|
||||||
|
|
||||||
|
if (isUpdateMode.value) {
|
||||||
|
await apiClient.extension.storage.policy.updatestorageHaloRunV1alpha1Policy(
|
||||||
|
formState.value.metadata.name,
|
||||||
|
formState.value
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await apiClient.extension.storage.policy.createstorageHaloRunV1alpha1Policy(
|
||||||
|
formState.value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onVisibleChange(false);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to save attachment policy", e);
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetForm = () => {
|
||||||
|
formState.value = cloneDeep(initialFormState);
|
||||||
|
formState.value.metadata.name = uuid();
|
||||||
|
reset("local-policy-form");
|
||||||
|
};
|
||||||
|
|
||||||
|
const { Command_Enter } = useMagicKeys();
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (Command_Enter.value && props.visible) {
|
||||||
|
submitForm("local-policy-form");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
(visible) => {
|
||||||
|
if (!visible) {
|
||||||
|
setTimeout(() => {
|
||||||
|
policyTemplate.value = undefined;
|
||||||
|
handleResetForm();
|
||||||
|
handleResetSettingForm();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.policy,
|
||||||
|
async (policy) => {
|
||||||
|
if (policy) {
|
||||||
|
formState.value = cloneDeep(policy);
|
||||||
|
|
||||||
|
// Get policy template
|
||||||
|
if (formState.value.spec.templateRef?.name) {
|
||||||
|
const { data } =
|
||||||
|
await apiClient.extension.storage.policyTemplate.getstorageHaloRunV1alpha1PolicyTemplate(
|
||||||
|
formState.value.spec.templateRef.name
|
||||||
|
);
|
||||||
|
policyTemplate.value = data;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setTimeout(() => {
|
||||||
|
policyTemplate.value = undefined;
|
||||||
|
handleResetForm();
|
||||||
|
handleResetSettingForm();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const onVisibleChange = (visible: boolean) => {
|
||||||
|
emit("update:visible", visible);
|
||||||
|
if (!visible) {
|
||||||
|
emit("close");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<VModal
|
||||||
|
:title="modalTitle"
|
||||||
|
:visible="visible"
|
||||||
|
:width="600"
|
||||||
|
@update:visible="onVisibleChange"
|
||||||
|
>
|
||||||
|
<FormKit
|
||||||
|
v-if="formSchema && configMapFormData"
|
||||||
|
id="local-policy-form"
|
||||||
|
v-model="configMapFormData['default']"
|
||||||
|
:actions="false"
|
||||||
|
:preserve="true"
|
||||||
|
type="form"
|
||||||
|
@submit="handleSave"
|
||||||
|
>
|
||||||
|
<FormKit
|
||||||
|
v-model="formState.spec.displayName"
|
||||||
|
label="名称"
|
||||||
|
type="text"
|
||||||
|
validation="required"
|
||||||
|
></FormKit>
|
||||||
|
<FormKitSchema :schema="formSchema" />
|
||||||
|
</FormKit>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<VSpace>
|
||||||
|
<VButton
|
||||||
|
:loading="saving"
|
||||||
|
type="secondary"
|
||||||
|
@click="$formkit.submit('local-policy-form')"
|
||||||
|
>
|
||||||
|
保存 ⌘ + ↵
|
||||||
|
</VButton>
|
||||||
|
<VButton @click="onVisibleChange(false)">取消 Esc</VButton>
|
||||||
|
</VSpace>
|
||||||
|
</template>
|
||||||
|
</VModal>
|
||||||
|
</template>
|
|
@ -1,78 +0,0 @@
|
||||||
<script lang="ts" setup>
|
|
||||||
import { VButton, VCard, VModal } from "@halo-dev/components";
|
|
||||||
|
|
||||||
withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
visible: boolean;
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
visible: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: "update:visible", visible: boolean): void;
|
|
||||||
(event: "close"): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const attachments = Array.from(new Array(50), (_, index) => index).map(
|
|
||||||
(index) => {
|
|
||||||
return {
|
|
||||||
id: index,
|
|
||||||
name: `attachment-${index}`,
|
|
||||||
url: `https://picsum.photos/1000/700?random=${index}`,
|
|
||||||
size: "1.2MB",
|
|
||||||
type: "image/png",
|
|
||||||
strategy: "本地存储",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const onVisibleChange = (visible: boolean) => {
|
|
||||||
emit("update:visible", visible);
|
|
||||||
if (!visible) {
|
|
||||||
emit("close");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<VModal
|
|
||||||
:visible="visible"
|
|
||||||
:width="1240"
|
|
||||||
title="选择附件"
|
|
||||||
@update:visible="onVisibleChange"
|
|
||||||
>
|
|
||||||
<div class="w-full">
|
|
||||||
<ul
|
|
||||||
class="grid grid-cols-2 gap-x-2 gap-y-3 sm:grid-cols-3 md:grid-cols-2 xl:grid-cols-8 2xl:grid-cols-8"
|
|
||||||
role="list"
|
|
||||||
>
|
|
||||||
<li
|
|
||||||
v-for="(attachment, index) in attachments"
|
|
||||||
:key="index"
|
|
||||||
class="relative"
|
|
||||||
>
|
|
||||||
<VCard :body-class="['!p-0']">
|
|
||||||
<div
|
|
||||||
class="group aspect-w-10 aspect-h-7 block w-full cursor-pointer overflow-hidden bg-gray-100"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
:src="attachment.url"
|
|
||||||
alt=""
|
|
||||||
class="pointer-events-none object-cover group-hover:opacity-75"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p
|
|
||||||
class="pointer-events-none block truncate px-2 py-1 text-sm font-medium text-gray-700"
|
|
||||||
>
|
|
||||||
{{ attachment.name }}
|
|
||||||
</p>
|
|
||||||
</VCard>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<template #footer>
|
|
||||||
<VButton type="secondary" @click="onVisibleChange(false)">确定</VButton>
|
|
||||||
</template>
|
|
||||||
</VModal>
|
|
||||||
</template>
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { VButton, VModal, VTabbar } from "@halo-dev/components";
|
||||||
|
import { ref, markRaw } from "vue";
|
||||||
|
import CoreSelectorProvider from "./selector-providers/CoreSelectorProvider.vue";
|
||||||
|
import UploadSelectorProvider from "./selector-providers/UploadSelectorProvider.vue";
|
||||||
|
import type {
|
||||||
|
AttachmentLike,
|
||||||
|
AttachmentSelectorPublicState,
|
||||||
|
} from "@halo-dev/admin-shared";
|
||||||
|
import { useExtensionPointsState } from "@/composables/usePlugins";
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
visible: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
visible: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: "update:visible", visible: boolean): void;
|
||||||
|
(event: "close"): void;
|
||||||
|
(event: "select", attachments: AttachmentLike[]): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const selected = ref<AttachmentLike[]>([] as AttachmentLike[]);
|
||||||
|
|
||||||
|
const attachmentSelectorPublicState = ref<AttachmentSelectorPublicState>({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
id: "core",
|
||||||
|
label: "附件库",
|
||||||
|
component: markRaw(CoreSelectorProvider),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "upload",
|
||||||
|
label: "上传",
|
||||||
|
component: markRaw(UploadSelectorProvider),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
useExtensionPointsState("ATTACHMENT_SELECTOR", attachmentSelectorPublicState);
|
||||||
|
|
||||||
|
const activeId = ref(attachmentSelectorPublicState.value.providers[0].id);
|
||||||
|
|
||||||
|
const onVisibleChange = (visible: boolean) => {
|
||||||
|
emit("update:visible", visible);
|
||||||
|
if (!visible) {
|
||||||
|
emit("close");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
console.log(Array.from(selected.value));
|
||||||
|
emit("select", Array.from(selected.value));
|
||||||
|
onVisibleChange(false);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<VModal
|
||||||
|
:visible="visible"
|
||||||
|
:width="1240"
|
||||||
|
title="选择附件"
|
||||||
|
height="calc(100vh - 20px)"
|
||||||
|
@update:visible="onVisibleChange"
|
||||||
|
>
|
||||||
|
<VTabbar
|
||||||
|
v-model:active-id="activeId"
|
||||||
|
:items="attachmentSelectorPublicState.providers"
|
||||||
|
class="w-full !rounded-none"
|
||||||
|
type="outline"
|
||||||
|
></VTabbar>
|
||||||
|
|
||||||
|
<div v-if="visible" class="mt-2">
|
||||||
|
<template
|
||||||
|
v-for="(provider, index) in attachmentSelectorPublicState.providers"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<Suspense>
|
||||||
|
<component
|
||||||
|
:is="provider.component"
|
||||||
|
v-if="activeId === provider.id"
|
||||||
|
v-model:selected="selected"
|
||||||
|
></component>
|
||||||
|
<template #fallback> 加载中 </template>
|
||||||
|
</Suspense>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<VButton type="secondary" @click="handleConfirm">
|
||||||
|
确定
|
||||||
|
<span v-if="selected.length">
|
||||||
|
(已选择 {{ selected.length }} 项)
|
||||||
|
</span>
|
||||||
|
</VButton>
|
||||||
|
</template>
|
||||||
|
</VModal>
|
||||||
|
</template>
|
|
@ -1,154 +0,0 @@
|
||||||
<script lang="ts" setup>
|
|
||||||
import {
|
|
||||||
IconAddCircle,
|
|
||||||
IconMore,
|
|
||||||
VButton,
|
|
||||||
VModal,
|
|
||||||
VSpace,
|
|
||||||
} from "@halo-dev/components";
|
|
||||||
import AttachmentLocalStrategyEditingModal from "./AttachmentLocalStrategyEditingModal.vue";
|
|
||||||
import AttachmentAliOSSStrategyEditingModal from "./AttachmentAliOSSStrategyEditingModal.vue";
|
|
||||||
import { ref } from "vue";
|
|
||||||
|
|
||||||
withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
visible: boolean;
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
visible: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: "update:visible", visible: boolean): void;
|
|
||||||
(event: "close"): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const strategies = ref([
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
name: "本地存储",
|
|
||||||
description: "~/.halo/uploads",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "2",
|
|
||||||
name: "阿里云 OSS",
|
|
||||||
description: "bucket/blog-attachments",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "3",
|
|
||||||
name: "阿里云 OSS",
|
|
||||||
description: "bucket/blog-photos",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const localStrategyVisible = ref(false);
|
|
||||||
const aliOSSStrategyVisible = ref(false);
|
|
||||||
|
|
||||||
function onVisibleChange(visible: boolean) {
|
|
||||||
emit("update:visible", visible);
|
|
||||||
if (!visible) {
|
|
||||||
emit("close");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<VModal
|
|
||||||
:visible="visible"
|
|
||||||
:width="750"
|
|
||||||
title="存储策略"
|
|
||||||
@update:visible="onVisibleChange"
|
|
||||||
>
|
|
||||||
<template #actions>
|
|
||||||
<FloatingDropdown>
|
|
||||||
<div v-tooltip="`添加存储策略`" class="modal-header-action">
|
|
||||||
<IconAddCircle />
|
|
||||||
</div>
|
|
||||||
<template #popper>
|
|
||||||
<div class="w-72 p-4">
|
|
||||||
<ul class="space-y-1">
|
|
||||||
<li
|
|
||||||
v-close-popper
|
|
||||||
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
|
||||||
@click="localStrategyVisible = true"
|
|
||||||
>
|
|
||||||
<span class="truncate">本地</span>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
v-close-popper
|
|
||||||
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
|
||||||
@click="aliOSSStrategyVisible = true"
|
|
||||||
>
|
|
||||||
<span class="truncate">阿里云 OSS</span>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
v-close-popper
|
|
||||||
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
|
||||||
>
|
|
||||||
<span class="truncate">Amazon S3</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</FloatingDropdown>
|
|
||||||
</template>
|
|
||||||
<ul class="box-border h-full w-full divide-y divide-gray-100" role="list">
|
|
||||||
<li v-for="(strategy, index) in strategies" :key="index">
|
|
||||||
<div
|
|
||||||
class="relative block cursor-pointer px-4 py-3 transition-all hover:bg-gray-50"
|
|
||||||
>
|
|
||||||
<div class="relative flex flex-row items-center">
|
|
||||||
<div class="flex-1">
|
|
||||||
<div class="flex flex-col sm:flex-row">
|
|
||||||
<span
|
|
||||||
class="mr-0 truncate text-sm font-medium text-gray-900 sm:mr-2"
|
|
||||||
>
|
|
||||||
{{ strategy.name }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="mt-1 flex">
|
|
||||||
<span class="text-xs text-gray-500">
|
|
||||||
{{ strategy.description }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex">
|
|
||||||
<div
|
|
||||||
class="inline-flex flex-col flex-col-reverse items-end gap-4 sm:flex-row sm:items-center sm:gap-6"
|
|
||||||
>
|
|
||||||
<time class="text-sm text-gray-500" datetime="2020-01-07">
|
|
||||||
2020-01-07
|
|
||||||
</time>
|
|
||||||
<span class="cursor-pointer">
|
|
||||||
<FloatingDropdown>
|
|
||||||
<IconMore />
|
|
||||||
<template #popper>
|
|
||||||
<div class="w-48 p-2">
|
|
||||||
<VSpace class="w-full" direction="column">
|
|
||||||
<VButton v-close-popper block type="secondary">
|
|
||||||
编辑
|
|
||||||
</VButton>
|
|
||||||
<VButton v-close-popper block type="danger">
|
|
||||||
删除
|
|
||||||
</VButton>
|
|
||||||
</VSpace>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</FloatingDropdown>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<template #footer>
|
|
||||||
<VButton @click="onVisibleChange(false)">关闭 Esc</VButton>
|
|
||||||
</template>
|
|
||||||
</VModal>
|
|
||||||
|
|
||||||
<AttachmentLocalStrategyEditingModal v-model:visible="localStrategyVisible" />
|
|
||||||
<AttachmentAliOSSStrategyEditingModal
|
|
||||||
v-model:visible="aliOSSStrategyVisible"
|
|
||||||
/>
|
|
||||||
</template>
|
|
|
@ -1,19 +1,20 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { VModal } from "@halo-dev/components";
|
import { VModal } from "@halo-dev/components";
|
||||||
import vueFilePond from "vue-filepond";
|
import FilePondUpload from "@/components/upload/FilePondUpload.vue";
|
||||||
import "filepond/dist/filepond.min.css";
|
import { computed, ref, watch, watchEffect } from "vue";
|
||||||
import FilePondPluginImagePreview from "filepond-plugin-image-preview";
|
import { apiClient } from "@halo-dev/admin-shared";
|
||||||
import "filepond-plugin-image-preview/dist/filepond-plugin-image-preview.min.css";
|
import type { Policy, Group } from "@halo-dev/api-client";
|
||||||
import { ref } from "vue";
|
import { useFetchAttachmentPolicy } from "../composables/use-attachment-policy";
|
||||||
|
import AttachmentPoliciesModal from "./AttachmentPoliciesModal.vue";
|
||||||
|
|
||||||
const FilePond = vueFilePond(FilePondPluginImagePreview);
|
const props = withDefaults(
|
||||||
|
|
||||||
withDefaults(
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
group?: Group;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
visible: false,
|
visible: false,
|
||||||
|
group: undefined,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -22,64 +23,95 @@ const emit = defineEmits<{
|
||||||
(event: "close"): void;
|
(event: "close"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const strategies = ref([
|
const { policies, handleFetchPolicies } = useFetchAttachmentPolicy();
|
||||||
{
|
|
||||||
id: "1",
|
const selectedPolicy = ref<Policy | null>(null);
|
||||||
name: "本地存储",
|
const policyVisible = ref(false);
|
||||||
description: "~/.halo/uploads",
|
const FilePondUploadRef = ref();
|
||||||
},
|
|
||||||
{
|
const modalTitle = computed(() => {
|
||||||
id: "2",
|
if (props.group && props.group.metadata.name) {
|
||||||
name: "阿里云 OSS",
|
return `上传附件:${props.group.spec.displayName}`;
|
||||||
description: "bucket/blog-attachments",
|
}
|
||||||
},
|
return "上传附件";
|
||||||
{
|
});
|
||||||
id: "3",
|
|
||||||
name: "阿里云 OSS",
|
watchEffect(() => {
|
||||||
description: "bucket/blog-photos",
|
if (policies.value.length) {
|
||||||
},
|
selectedPolicy.value = policies.value[0];
|
||||||
]);
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const onVisibleChange = (visible: boolean) => {
|
const onVisibleChange = (visible: boolean) => {
|
||||||
emit("update:visible", visible);
|
emit("update:visible", visible);
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
emit("close");
|
emit("close");
|
||||||
|
policyVisible.value = false;
|
||||||
|
FilePondUploadRef.value.handleRemoveFiles();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const uploadHandler = computed(() => {
|
||||||
|
return (file, config) =>
|
||||||
|
apiClient.extension.storage.attachment.uploadAttachment(
|
||||||
|
file,
|
||||||
|
selectedPolicy.value?.metadata.name as string,
|
||||||
|
props.group?.metadata.name as string,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
handleFetchPolicies();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<VModal
|
<VModal
|
||||||
:body-class="['!p-0']"
|
:body-class="['!p-0']"
|
||||||
:visible="visible"
|
:visible="visible"
|
||||||
:width="600"
|
:width="600"
|
||||||
title="上传附件"
|
:title="modalTitle"
|
||||||
@update:visible="onVisibleChange"
|
@update:visible="onVisibleChange"
|
||||||
>
|
>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<FloatingDropdown>
|
<FloatingDropdown>
|
||||||
<div v-tooltip="`选择存储策略`" class="modal-header-action">
|
<div v-tooltip="`选择存储策略`" class="modal-header-action">
|
||||||
<span class="text-sm">本地存储</span>
|
<span class="text-sm">
|
||||||
|
{{ selectedPolicy?.spec.displayName || "无存储策略" }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<template #popper>
|
<template #popper>
|
||||||
<div class="w-72 p-4">
|
<div class="w-72 p-4">
|
||||||
<ul class="space-y-1">
|
<ul class="space-y-1">
|
||||||
<li
|
<li
|
||||||
v-for="(strategy, index) in strategies"
|
v-for="(policy, index) in policies"
|
||||||
:key="index"
|
:key="index"
|
||||||
v-close-popper
|
v-close-popper
|
||||||
class="flex cursor-pointer flex-col rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
:class="{
|
||||||
|
'!bg-gray-100 !text-gray-900':
|
||||||
|
selectedPolicy?.metadata.name === policy.metadata.name,
|
||||||
|
}"
|
||||||
|
class="flex cursor-pointer flex-col rounded px-2 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
||||||
|
@click="selectedPolicy = policy"
|
||||||
>
|
>
|
||||||
<span class="truncate">
|
<span class="truncate">
|
||||||
{{ strategy.name }}
|
{{ policy.spec.displayName }}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-xs">
|
<span class="text-xs">
|
||||||
{{ strategy.description }}
|
{{ policy.spec.templateRef?.name }}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
v-close-popper
|
||||||
|
class="flex cursor-pointer flex-col rounded px-2 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
||||||
|
@click="policyVisible = true"
|
||||||
>
|
>
|
||||||
<span class="truncate">新增存储策略</span>
|
<span class="truncate"> 新增存储策略 </span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -87,14 +119,21 @@ const onVisibleChange = (visible: boolean) => {
|
||||||
</FloatingDropdown>
|
</FloatingDropdown>
|
||||||
</template>
|
</template>
|
||||||
<div class="w-full p-4">
|
<div class="w-full p-4">
|
||||||
<file-pond
|
<FilePondUpload
|
||||||
ref="pond"
|
ref="FilePondUploadRef"
|
||||||
accepted-file-types="image/jpeg, image/png"
|
|
||||||
label-idle="Drop files here..."
|
|
||||||
name="test"
|
|
||||||
server="/api"
|
|
||||||
:allow-multiple="true"
|
:allow-multiple="true"
|
||||||
|
:handler="uploadHandler"
|
||||||
|
:disabled="!selectedPolicy"
|
||||||
|
:max-parallel-uploads="5"
|
||||||
|
:label-idle="
|
||||||
|
selectedPolicy ? '点击选择文件或者拖拽文件到此处' : '请先选择存储策略'
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</VModal>
|
</VModal>
|
||||||
|
|
||||||
|
<AttachmentPoliciesModal
|
||||||
|
v-model:visible="policyVisible"
|
||||||
|
@close="handleFetchPolicies"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -0,0 +1,205 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {
|
||||||
|
IconCheckboxFill,
|
||||||
|
IconArrowLeft,
|
||||||
|
IconArrowRight,
|
||||||
|
VCard,
|
||||||
|
VEmpty,
|
||||||
|
VSpace,
|
||||||
|
VButton,
|
||||||
|
IconUpload,
|
||||||
|
VPagination,
|
||||||
|
IconEye,
|
||||||
|
IconCheckboxCircle,
|
||||||
|
} from "@halo-dev/components";
|
||||||
|
import { watchEffect, ref } from "vue";
|
||||||
|
import { isImage } from "@/utils/image";
|
||||||
|
import { useAttachmentControl } from "../../composables/use-attachment";
|
||||||
|
import LazyImage from "@/components/image/LazyImage.vue";
|
||||||
|
import type { AttachmentLike } from "@halo-dev/admin-shared";
|
||||||
|
import type { Attachment, Group } from "@halo-dev/api-client";
|
||||||
|
import AttachmentUploadModal from "../AttachmentUploadModal.vue";
|
||||||
|
import AttachmentFileTypeIcon from "../AttachmentFileTypeIcon.vue";
|
||||||
|
import AttachmentDetailModal from "../AttachmentDetailModal.vue";
|
||||||
|
import AttachmentGroupList from "../AttachmentGroupList.vue";
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
selected: AttachmentLike[];
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
selected: () => [],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: "update:selected", attachments: AttachmentLike[]): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const selectedGroup = ref<Group>();
|
||||||
|
|
||||||
|
const {
|
||||||
|
attachments,
|
||||||
|
loading,
|
||||||
|
selectedAttachment,
|
||||||
|
selectedAttachments,
|
||||||
|
handleFetchAttachments,
|
||||||
|
handlePaginationChange,
|
||||||
|
handleSelect,
|
||||||
|
handleSelectPrevious,
|
||||||
|
handleSelectNext,
|
||||||
|
handleReset,
|
||||||
|
isChecked,
|
||||||
|
} = useAttachmentControl({ group: selectedGroup });
|
||||||
|
|
||||||
|
const uploadVisible = ref(false);
|
||||||
|
const detailVisible = ref(false);
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
emit("update:selected", Array.from(selectedAttachments.value));
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleOpenDetail = (attachment: Attachment) => {
|
||||||
|
selectedAttachment.value = attachment;
|
||||||
|
detailVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onGroupChange = () => {
|
||||||
|
handleReset();
|
||||||
|
handleFetchAttachments();
|
||||||
|
};
|
||||||
|
|
||||||
|
await handleFetchAttachments();
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<AttachmentGroupList
|
||||||
|
v-model:selected-group="selectedGroup"
|
||||||
|
readonly
|
||||||
|
@select="onGroupChange"
|
||||||
|
/>
|
||||||
|
<VEmpty
|
||||||
|
v-if="!attachments.total && !loading"
|
||||||
|
message="当前没有附件,你可以尝试刷新或者上传附件"
|
||||||
|
title="当前没有附件"
|
||||||
|
>
|
||||||
|
<template #actions>
|
||||||
|
<VSpace>
|
||||||
|
<VButton @click="handleFetchAttachments">刷新</VButton>
|
||||||
|
<VButton type="secondary" @click="uploadVisible = true">
|
||||||
|
<template #icon>
|
||||||
|
<IconUpload class="h-full w-full" />
|
||||||
|
</template>
|
||||||
|
上传附件
|
||||||
|
</VButton>
|
||||||
|
</VSpace>
|
||||||
|
</template>
|
||||||
|
</VEmpty>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="mt-2 grid grid-cols-3 gap-x-2 gap-y-3 sm:grid-cols-3 md:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-10"
|
||||||
|
role="list"
|
||||||
|
>
|
||||||
|
<VCard
|
||||||
|
v-for="(attachment, index) in attachments.items"
|
||||||
|
:key="index"
|
||||||
|
:body-class="['!p-0']"
|
||||||
|
:class="{
|
||||||
|
'ring-1 ring-primary': isChecked(attachment),
|
||||||
|
}"
|
||||||
|
class="hover:shadow"
|
||||||
|
@click.stop="handleSelect(attachment)"
|
||||||
|
>
|
||||||
|
<div class="group relative bg-white">
|
||||||
|
<div
|
||||||
|
class="aspect-w-10 aspect-h-8 block h-full w-full cursor-pointer overflow-hidden bg-gray-100"
|
||||||
|
>
|
||||||
|
<LazyImage
|
||||||
|
v-if="isImage(attachment.spec.mediaType)"
|
||||||
|
:key="attachment.metadata.name"
|
||||||
|
:alt="attachment.spec.displayName"
|
||||||
|
:src="attachment.status?.permalink"
|
||||||
|
class="pointer-events-none object-cover group-hover:opacity-75"
|
||||||
|
>
|
||||||
|
<template #loading>
|
||||||
|
<div class="flex h-full items-center justify-center object-cover">
|
||||||
|
<span class="text-xs text-gray-400">加载中...</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #error>
|
||||||
|
<div class="flex h-full items-center justify-center object-cover">
|
||||||
|
<span class="text-xs text-red-400">加载异常</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</LazyImage>
|
||||||
|
<AttachmentFileTypeIcon
|
||||||
|
v-else
|
||||||
|
:file-name="attachment.spec.displayName"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="pointer-events-none block truncate px-2 py-1 text-center text-xs font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
{{ attachment.spec.displayName }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
:class="{ '!flex': selectedAttachments.has(attachment) }"
|
||||||
|
class="absolute top-0 left-0 hidden h-1/3 w-full justify-end bg-gradient-to-b from-gray-300 to-transparent ease-in-out group-hover:flex"
|
||||||
|
>
|
||||||
|
<IconEye
|
||||||
|
class="mt-1 mr-1 hidden h-6 w-6 cursor-pointer text-white transition-all hover:text-primary group-hover:block"
|
||||||
|
@click.stop="handleOpenDetail(attachment)"
|
||||||
|
/>
|
||||||
|
<IconCheckboxFill
|
||||||
|
:class="{
|
||||||
|
'!text-primary': selectedAttachments.has(attachment),
|
||||||
|
}"
|
||||||
|
class="mt-1 mr-1 h-6 w-6 cursor-pointer text-white transition-all hover:text-primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</VCard>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 bg-white sm:flex sm:items-center sm:justify-end">
|
||||||
|
<VPagination
|
||||||
|
:page="attachments.page"
|
||||||
|
:size="attachments.size"
|
||||||
|
:total="attachments.total"
|
||||||
|
@change="handlePaginationChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<AttachmentUploadModal
|
||||||
|
v-model:visible="uploadVisible"
|
||||||
|
@close="handleFetchAttachments"
|
||||||
|
/>
|
||||||
|
<AttachmentDetailModal
|
||||||
|
v-model:visible="detailVisible"
|
||||||
|
:mount-to-body="true"
|
||||||
|
:attachment="selectedAttachment"
|
||||||
|
@close="selectedAttachment = undefined"
|
||||||
|
>
|
||||||
|
<template #actions>
|
||||||
|
<div
|
||||||
|
v-if="selectedAttachment && selectedAttachments.has(selectedAttachment)"
|
||||||
|
class="modal-header-action"
|
||||||
|
@click="handleSelect(selectedAttachment)"
|
||||||
|
>
|
||||||
|
<IconCheckboxFill />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="modal-header-action"
|
||||||
|
@click="handleSelect(selectedAttachment)"
|
||||||
|
>
|
||||||
|
<IconCheckboxCircle />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-header-action" @click="handleSelectPrevious">
|
||||||
|
<IconArrowLeft />
|
||||||
|
</div>
|
||||||
|
<div class="modal-header-action" @click="handleSelectNext">
|
||||||
|
<IconArrowRight />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</AttachmentDetailModal>
|
||||||
|
</template>
|
|
@ -0,0 +1,234 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {
|
||||||
|
VEmpty,
|
||||||
|
IconCheckboxFill,
|
||||||
|
VCard,
|
||||||
|
IconDeleteBin,
|
||||||
|
useDialog,
|
||||||
|
} from "@halo-dev/components";
|
||||||
|
|
||||||
|
import { apiClient, type AttachmentLike } from "@halo-dev/admin-shared";
|
||||||
|
import LazyImage from "@/components/image/LazyImage.vue";
|
||||||
|
import type { Attachment } from "@halo-dev/api-client";
|
||||||
|
import FilePondUpload from "@/components/upload/FilePondUpload.vue";
|
||||||
|
import AttachmentFileTypeIcon from "../AttachmentFileTypeIcon.vue";
|
||||||
|
import { computed, ref, watchEffect } from "vue";
|
||||||
|
import type { AxiosResponse } from "axios";
|
||||||
|
import { isImage } from "@/utils/image";
|
||||||
|
import { useFetchAttachmentPolicy } from "../../composables/use-attachment-policy";
|
||||||
|
import { useFetchAttachmentGroup } from "../../composables/use-attachment-group";
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
selected: AttachmentLike[];
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
selected: () => [],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: "update:selected", attachments: AttachmentLike[]): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { policies } = useFetchAttachmentPolicy({ fetchOnMounted: true });
|
||||||
|
const policyMap = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: "选择存储策略",
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
...policies.value.map((policy) => {
|
||||||
|
return {
|
||||||
|
label: policy.spec.displayName,
|
||||||
|
value: policy.metadata.name,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
const selectedPolicy = ref("");
|
||||||
|
|
||||||
|
const { groups } = useFetchAttachmentGroup({ fetchOnMounted: true });
|
||||||
|
const groupMap = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: "选择分组",
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
...groups.value.map((group) => {
|
||||||
|
return {
|
||||||
|
label: group.spec.displayName,
|
||||||
|
value: group.metadata.name,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
const selectedGroup = ref("");
|
||||||
|
|
||||||
|
const attachments = ref<Set<Attachment>>(new Set<Attachment>());
|
||||||
|
const selectedAttachments = ref<Set<Attachment>>(new Set<Attachment>());
|
||||||
|
|
||||||
|
const uploadHandler = computed(() => {
|
||||||
|
return (file, config) =>
|
||||||
|
apiClient.extension.storage.attachment.uploadAttachment(
|
||||||
|
file,
|
||||||
|
selectedPolicy.value,
|
||||||
|
selectedGroup.value,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const onUploaded = async (response: AxiosResponse) => {
|
||||||
|
const attachment = response.data as Attachment;
|
||||||
|
|
||||||
|
const { data } =
|
||||||
|
await apiClient.extension.storage.attachment.getstorageHaloRunV1alpha1Attachment(
|
||||||
|
attachment.metadata.name
|
||||||
|
);
|
||||||
|
attachments.value.add(data);
|
||||||
|
selectedAttachments.value.add(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelect = async (attachment: Attachment | undefined) => {
|
||||||
|
if (!attachment) return;
|
||||||
|
if (selectedAttachments.value.has(attachment)) {
|
||||||
|
selectedAttachments.value.delete(attachment);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedAttachments.value.add(attachment);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dialog = useDialog();
|
||||||
|
|
||||||
|
const handleDelete = async (attachment: Attachment) => {
|
||||||
|
dialog.warning({
|
||||||
|
title: "确定要删除当前的附件吗?",
|
||||||
|
confirmType: "danger",
|
||||||
|
onConfirm: async () => {
|
||||||
|
try {
|
||||||
|
await apiClient.extension.storage.attachment.deletestorageHaloRunV1alpha1Attachment(
|
||||||
|
attachment.metadata.name
|
||||||
|
);
|
||||||
|
attachments.value.delete(attachment);
|
||||||
|
selectedAttachments.value.delete(attachment);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to delete attachment", e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
emit("update:selected", Array.from(selectedAttachments.value));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex h-full flex-col gap-4 sm:flex-row">
|
||||||
|
<div class="h-full w-full space-y-4 overflow-auto sm:w-96">
|
||||||
|
<FormKit type="form">
|
||||||
|
<FormKit
|
||||||
|
v-model="selectedPolicy"
|
||||||
|
type="select"
|
||||||
|
:options="policyMap"
|
||||||
|
label="存储策略"
|
||||||
|
></FormKit>
|
||||||
|
<FormKit
|
||||||
|
v-model="selectedGroup"
|
||||||
|
:options="groupMap"
|
||||||
|
type="select"
|
||||||
|
label="分组"
|
||||||
|
></FormKit>
|
||||||
|
</FormKit>
|
||||||
|
<FilePondUpload
|
||||||
|
ref="FilePondUploadRef"
|
||||||
|
:allow-multiple="true"
|
||||||
|
:handler="uploadHandler"
|
||||||
|
:max-parallel-uploads="5"
|
||||||
|
:disabled="!selectedPolicy"
|
||||||
|
:label-idle="
|
||||||
|
selectedPolicy ? '点击选择文件或者拖拽文件到此处' : '请先选择存储策略'
|
||||||
|
"
|
||||||
|
@uploaded="onUploaded"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="h-full flex-1 overflow-auto">
|
||||||
|
<VEmpty
|
||||||
|
v-if="!attachments.size"
|
||||||
|
message="当前没有上传的文件,你可以点击左侧区域上传文件"
|
||||||
|
title="当前没有上传的文件"
|
||||||
|
>
|
||||||
|
</VEmpty>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="grid grid-cols-3 gap-x-2 gap-y-3 p-0.5 sm:grid-cols-3 md:grid-cols-4 xl:grid-cols-4 2xl:grid-cols-6"
|
||||||
|
role="list"
|
||||||
|
>
|
||||||
|
<VCard
|
||||||
|
v-for="(attachment, index) in Array.from(attachments)"
|
||||||
|
:key="index"
|
||||||
|
:body-class="['!p-0']"
|
||||||
|
:class="{
|
||||||
|
'ring-1 ring-primary': selectedAttachments.has(attachment),
|
||||||
|
}"
|
||||||
|
class="hover:shadow"
|
||||||
|
@click="handleSelect(attachment)"
|
||||||
|
>
|
||||||
|
<div class="group relative bg-white">
|
||||||
|
<div
|
||||||
|
class="aspect-w-10 aspect-h-8 block h-full w-full cursor-pointer overflow-hidden bg-gray-100"
|
||||||
|
>
|
||||||
|
<LazyImage
|
||||||
|
v-if="isImage(attachment.spec.mediaType)"
|
||||||
|
:key="attachment.metadata.name"
|
||||||
|
:alt="attachment.spec.displayName"
|
||||||
|
:src="attachment.status?.permalink"
|
||||||
|
class="pointer-events-none object-cover group-hover:opacity-75"
|
||||||
|
>
|
||||||
|
<template #loading>
|
||||||
|
<div
|
||||||
|
class="flex h-full items-center justify-center object-cover"
|
||||||
|
>
|
||||||
|
<span class="text-xs text-gray-400">加载中...</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #error>
|
||||||
|
<div
|
||||||
|
class="flex h-full items-center justify-center object-cover"
|
||||||
|
>
|
||||||
|
<span class="text-xs text-red-400">加载异常</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</LazyImage>
|
||||||
|
<AttachmentFileTypeIcon
|
||||||
|
v-else
|
||||||
|
:file-name="attachment.spec.displayName"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="pointer-events-none block truncate px-2 py-1 text-center text-xs font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
{{ attachment.spec.displayName }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
:class="{ '!flex': selectedAttachments.has(attachment) }"
|
||||||
|
class="absolute top-0 left-0 hidden h-1/3 w-full justify-end bg-gradient-to-b from-gray-300 to-transparent ease-in-out group-hover:flex"
|
||||||
|
>
|
||||||
|
<IconDeleteBin
|
||||||
|
class="mt-1 mr-1 hidden h-5 w-5 cursor-pointer text-red-400 transition-all hover:text-red-600 group-hover:block"
|
||||||
|
@click.stop="handleDelete(attachment)"
|
||||||
|
/>
|
||||||
|
<IconCheckboxFill
|
||||||
|
:class="{
|
||||||
|
'!text-primary': selectedAttachments.has(attachment),
|
||||||
|
}"
|
||||||
|
class="mt-1 mr-1 h-5 w-5 cursor-pointer text-white transition-all hover:text-primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</VCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { onMounted, ref, type Ref } from "vue";
|
||||||
|
import type { Group } from "@halo-dev/api-client";
|
||||||
|
import { apiClient } from "@halo-dev/admin-shared";
|
||||||
|
|
||||||
|
interface useFetchAttachmentGroupReturn {
|
||||||
|
groups: Ref<Group[]>;
|
||||||
|
loading: Ref<boolean>;
|
||||||
|
handleFetchGroups: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFetchAttachmentGroup(options?: {
|
||||||
|
fetchOnMounted: boolean;
|
||||||
|
}): useFetchAttachmentGroupReturn {
|
||||||
|
const { fetchOnMounted } = options || {};
|
||||||
|
|
||||||
|
const groups = ref<Group[]>([] as Group[]);
|
||||||
|
const loading = ref<boolean>(false);
|
||||||
|
|
||||||
|
const handleFetchGroups = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const { data } =
|
||||||
|
await apiClient.extension.storage.group.liststorageHaloRunV1alpha1Group();
|
||||||
|
groups.value = data.items;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to fetch attachment groups", e);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchOnMounted && handleFetchGroups();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
groups,
|
||||||
|
loading,
|
||||||
|
handleFetchGroups,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
import type { Ref } from "vue";
|
||||||
|
import type { Policy } from "@halo-dev/api-client";
|
||||||
|
import { apiClient } from "@halo-dev/admin-shared";
|
||||||
|
|
||||||
|
interface useFetchAttachmentPolicyReturn {
|
||||||
|
policies: Ref<Policy[]>;
|
||||||
|
loading: Ref<boolean>;
|
||||||
|
handleFetchPolicies: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFetchAttachmentPolicy(options?: {
|
||||||
|
fetchOnMounted: boolean;
|
||||||
|
}): useFetchAttachmentPolicyReturn {
|
||||||
|
const { fetchOnMounted } = options || {};
|
||||||
|
|
||||||
|
const policies = ref<Policy[]>([] as Policy[]);
|
||||||
|
const loading = ref<boolean>(false);
|
||||||
|
|
||||||
|
const handleFetchPolicies = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const { data } =
|
||||||
|
await apiClient.extension.storage.policy.liststorageHaloRunV1alpha1Policy();
|
||||||
|
policies.value = data.items;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to fetch attachment policies", e);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchOnMounted && handleFetchPolicies();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
policies,
|
||||||
|
loading,
|
||||||
|
handleFetchPolicies,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,214 @@
|
||||||
|
import type {
|
||||||
|
Attachment,
|
||||||
|
AttachmentList,
|
||||||
|
Group,
|
||||||
|
Policy,
|
||||||
|
User,
|
||||||
|
} from "@halo-dev/api-client";
|
||||||
|
import type { Ref } from "vue";
|
||||||
|
import { ref, watch } from "vue";
|
||||||
|
import { apiClient } from "@halo-dev/admin-shared";
|
||||||
|
import { useDialog } from "@halo-dev/components";
|
||||||
|
|
||||||
|
interface useAttachmentControlReturn {
|
||||||
|
attachments: Ref<AttachmentList>;
|
||||||
|
loading: Ref<boolean>;
|
||||||
|
selectedAttachment: Ref<Attachment | undefined>;
|
||||||
|
selectedAttachments: Ref<Set<Attachment>>;
|
||||||
|
checkedAll: Ref<boolean>;
|
||||||
|
handleFetchAttachments: () => void;
|
||||||
|
handlePaginationChange: ({
|
||||||
|
page,
|
||||||
|
size,
|
||||||
|
}: {
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
}) => void;
|
||||||
|
handleSelectPrevious: () => void;
|
||||||
|
handleSelectNext: () => void;
|
||||||
|
handleDeleteInBatch: () => void;
|
||||||
|
handleCheckAll: (checkAll: boolean) => void;
|
||||||
|
handleSelect: (attachment: Attachment | undefined) => void;
|
||||||
|
isChecked: (attachment: Attachment) => boolean;
|
||||||
|
handleReset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAttachmentControl(filterOptions?: {
|
||||||
|
policy?: Ref<Policy | undefined>;
|
||||||
|
group?: Ref<Group | undefined>;
|
||||||
|
user?: Ref<User | undefined>;
|
||||||
|
keyword?: Ref<string | undefined>;
|
||||||
|
}): useAttachmentControlReturn {
|
||||||
|
const { user, policy, group, keyword } = filterOptions || {};
|
||||||
|
|
||||||
|
const attachments = ref<AttachmentList>({
|
||||||
|
page: 1,
|
||||||
|
size: 60,
|
||||||
|
total: 0,
|
||||||
|
items: [],
|
||||||
|
first: true,
|
||||||
|
last: false,
|
||||||
|
hasNext: false,
|
||||||
|
hasPrevious: false,
|
||||||
|
});
|
||||||
|
const loading = ref<boolean>(false);
|
||||||
|
|
||||||
|
const selectedAttachment = ref<Attachment>();
|
||||||
|
const selectedAttachments = ref<Set<Attachment>>(new Set<Attachment>());
|
||||||
|
const checkedAll = ref(false);
|
||||||
|
|
||||||
|
const dialog = useDialog();
|
||||||
|
|
||||||
|
const handleFetchAttachments = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const { data } =
|
||||||
|
await apiClient.extension.storage.attachment.searchAttachments(
|
||||||
|
policy?.value?.metadata.name,
|
||||||
|
keyword?.value,
|
||||||
|
group?.value?.metadata.name,
|
||||||
|
user?.value?.metadata.name,
|
||||||
|
attachments.value.size,
|
||||||
|
attachments.value.page,
|
||||||
|
[],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
attachments.value = data;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to fetch attachments", e);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePaginationChange = async ({
|
||||||
|
page,
|
||||||
|
size,
|
||||||
|
}: {
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
}) => {
|
||||||
|
attachments.value.page = page;
|
||||||
|
attachments.value.size = size;
|
||||||
|
await handleFetchAttachments();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectPrevious = async () => {
|
||||||
|
const { items, hasPrevious } = attachments.value;
|
||||||
|
const index = items.findIndex(
|
||||||
|
(attachment) =>
|
||||||
|
attachment.metadata.name === selectedAttachment.value?.metadata.name
|
||||||
|
);
|
||||||
|
if (index > 0) {
|
||||||
|
selectedAttachment.value = items[index - 1];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (index === 0 && hasPrevious) {
|
||||||
|
attachments.value.page--;
|
||||||
|
await handleFetchAttachments();
|
||||||
|
selectedAttachment.value =
|
||||||
|
attachments.value.items[attachments.value.items.length - 1];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectNext = async () => {
|
||||||
|
const { items, hasNext } = attachments.value;
|
||||||
|
const index = items.findIndex(
|
||||||
|
(attachment) =>
|
||||||
|
attachment.metadata.name === selectedAttachment.value?.metadata.name
|
||||||
|
);
|
||||||
|
if (index < items.length - 1) {
|
||||||
|
selectedAttachment.value = items[index + 1];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (index === items.length - 1 && hasNext) {
|
||||||
|
attachments.value.page++;
|
||||||
|
await handleFetchAttachments();
|
||||||
|
selectedAttachment.value = attachments.value.items[0];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteInBatch = () => {
|
||||||
|
dialog.warning({
|
||||||
|
title: "确定要删除所选的附件吗?",
|
||||||
|
description: "其中 20 个附件包含关联关系,删除之后将无法恢复",
|
||||||
|
confirmType: "danger",
|
||||||
|
onConfirm: async () => {
|
||||||
|
try {
|
||||||
|
const promises = Array.from(selectedAttachments.value).map(
|
||||||
|
(attachment) => {
|
||||||
|
return apiClient.extension.storage.attachment.deletestorageHaloRunV1alpha1Attachment(
|
||||||
|
attachment.metadata.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await Promise.all(promises);
|
||||||
|
selectedAttachments.value.clear();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to delete attachments", e);
|
||||||
|
} finally {
|
||||||
|
await handleFetchAttachments();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheckAll = (checkAll: boolean) => {
|
||||||
|
if (checkAll) {
|
||||||
|
attachments.value.items.forEach((attachment) => {
|
||||||
|
selectedAttachments.value.add(attachment);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
selectedAttachments.value.clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelect = async (attachment: Attachment | undefined) => {
|
||||||
|
if (!attachment) return;
|
||||||
|
if (selectedAttachments.value.has(attachment)) {
|
||||||
|
selectedAttachments.value.delete(attachment);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedAttachments.value.add(attachment);
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => selectedAttachments.value.size,
|
||||||
|
(newValue) => {
|
||||||
|
checkedAll.value = newValue === attachments.value.items?.length;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const isChecked = (attachment: Attachment) => {
|
||||||
|
return (
|
||||||
|
attachment.metadata.name === selectedAttachment.value?.metadata.name ||
|
||||||
|
Array.from(selectedAttachments.value)
|
||||||
|
.map((item) => item.metadata.name)
|
||||||
|
.includes(attachment.metadata.name)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
attachments.value.page = 1;
|
||||||
|
selectedAttachment.value = undefined;
|
||||||
|
selectedAttachments.value.clear();
|
||||||
|
checkedAll.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
attachments,
|
||||||
|
loading,
|
||||||
|
selectedAttachment,
|
||||||
|
selectedAttachments,
|
||||||
|
checkedAll,
|
||||||
|
handleFetchAttachments,
|
||||||
|
handlePaginationChange,
|
||||||
|
handleSelectPrevious,
|
||||||
|
handleSelectNext,
|
||||||
|
handleDeleteInBatch,
|
||||||
|
handleCheckAll,
|
||||||
|
handleSelect,
|
||||||
|
isChecked,
|
||||||
|
handleReset,
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,10 +1,11 @@
|
||||||
import { BasicLayout, definePlugin } from "@halo-dev/admin-shared";
|
import { BasicLayout, definePlugin } from "@halo-dev/admin-shared";
|
||||||
import AttachmentList from "./AttachmentList.vue";
|
import AttachmentList from "./AttachmentList.vue";
|
||||||
|
import AttachmentSelectorModal from "./components/AttachmentSelectorModal.vue";
|
||||||
import { IconFolder } from "@halo-dev/components";
|
import { IconFolder } from "@halo-dev/components";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "attachmentModule",
|
name: "attachmentModule",
|
||||||
components: [],
|
components: [AttachmentSelectorModal],
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: "/attachments",
|
path: "/attachments",
|
||||||
|
|
|
@ -8,33 +8,57 @@ import {
|
||||||
IconUserSettings,
|
IconUserSettings,
|
||||||
VCard,
|
VCard,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
|
import { markRaw, type Component } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
|
import type { RouteLocationRaw } from "vue-router";
|
||||||
|
|
||||||
const actions = [
|
interface Action {
|
||||||
|
icon: Component;
|
||||||
|
title: string;
|
||||||
|
route: RouteLocationRaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions: Action[] = [
|
||||||
{
|
{
|
||||||
icon: IconBookRead,
|
icon: markRaw(IconBookRead),
|
||||||
title: "创建文章",
|
title: "创建文章",
|
||||||
route: "PostEditor",
|
route: {
|
||||||
|
name: "PostEditor",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: IconFolder,
|
icon: markRaw(IconFolder),
|
||||||
title: "附件上传",
|
title: "附件上传",
|
||||||
route: "Attachments",
|
route: {
|
||||||
|
name: "Attachments",
|
||||||
|
query: {
|
||||||
|
action: "upload",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: IconPalette,
|
icon: markRaw(IconPalette),
|
||||||
title: "外观编辑",
|
title: "外观编辑",
|
||||||
route: "ThemeVisual",
|
route: {
|
||||||
|
name: "ThemeVisual",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: IconPlug,
|
icon: markRaw(IconPlug),
|
||||||
title: "插件管理",
|
title: "插件管理",
|
||||||
route: "Plugins",
|
route: {
|
||||||
|
name: "Plugins",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: IconUserSettings,
|
icon: markRaw(IconUserSettings),
|
||||||
title: "新建用户",
|
title: "新建用户",
|
||||||
route: "Users",
|
route: {
|
||||||
|
name: "Users",
|
||||||
|
query: {
|
||||||
|
action: "create",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -53,7 +77,7 @@ const router = useRouter();
|
||||||
v-for="(action, index) in actions"
|
v-for="(action, index) in actions"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="group relative cursor-pointer bg-white p-6 hover:bg-gray-50"
|
class="group relative cursor-pointer bg-white p-6 hover:bg-gray-50"
|
||||||
@click="router.push({ name: action.route })"
|
@click="router.push(action.route)"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<span
|
<span
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { apiClient } from "@halo-dev/admin-shared";
|
||||||
import type { User, UserList } from "@halo-dev/api-client";
|
import type { User, UserList } from "@halo-dev/api-client";
|
||||||
import { rbacAnnotations } from "@/constants/annotations";
|
import { rbacAnnotations } from "@/constants/annotations";
|
||||||
import { formatDatetime } from "@/utils/date";
|
import { formatDatetime } from "@/utils/date";
|
||||||
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
|
|
||||||
const checkAll = ref(false);
|
const checkAll = ref(false);
|
||||||
const editingModal = ref<boolean>(false);
|
const editingModal = ref<boolean>(false);
|
||||||
|
@ -66,6 +67,11 @@ const handleOpenCreateModal = (user: User) => {
|
||||||
editingModal.value = true;
|
editingModal.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onEditingModalClose = () => {
|
||||||
|
routeQueryAction.value = undefined;
|
||||||
|
handleFetchUsers();
|
||||||
|
};
|
||||||
|
|
||||||
const handleOpenPasswordChangeModal = (user: User) => {
|
const handleOpenPasswordChangeModal = (user: User) => {
|
||||||
selectedUser.value = user;
|
selectedUser.value = user;
|
||||||
passwordChangeModal.value = true;
|
passwordChangeModal.value = true;
|
||||||
|
@ -80,13 +86,25 @@ const getRoles = (user: User) => {
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
handleFetchUsers();
|
handleFetchUsers();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Route query action
|
||||||
|
const routeQueryAction = useRouteQuery<string | undefined>("action");
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!routeQueryAction.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (routeQueryAction.value === "create") {
|
||||||
|
editingModal.value = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<UserEditingModal
|
<UserEditingModal
|
||||||
v-model:visible="editingModal"
|
v-model:visible="editingModal"
|
||||||
v-permission="['system:users:manage']"
|
v-permission="['system:users:manage']"
|
||||||
:user="selectedUser"
|
:user="selectedUser"
|
||||||
@close="handleFetchUsers"
|
@close="onEditingModalClose"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UserPasswordChangeModal
|
<UserPasswordChangeModal
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
export const imageTypes: string[] = [
|
||||||
|
"image/jpeg",
|
||||||
|
"image/jpg",
|
||||||
|
"image/png",
|
||||||
|
"image/gif",
|
||||||
|
"image/webp",
|
||||||
|
"image/svg+xml",
|
||||||
|
];
|
||||||
|
|
||||||
|
export function isImage(mediaType: string | undefined): boolean {
|
||||||
|
if (!mediaType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return imageTypes.includes(mediaType);
|
||||||
|
}
|
|
@ -8,6 +8,7 @@
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
},
|
||||||
|
"types": ["unplugin-icons/types/vue"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { VitePWA } from "vite-plugin-pwa";
|
||||||
import { viteExternalsPlugin as ViteExternals } from "vite-plugin-externals";
|
import { viteExternalsPlugin as ViteExternals } from "vite-plugin-externals";
|
||||||
import { viteStaticCopy as ViteStaticCopy } from "vite-plugin-static-copy";
|
import { viteStaticCopy as ViteStaticCopy } from "vite-plugin-static-copy";
|
||||||
import { createHtmlPlugin as VitePluginHtml } from "vite-plugin-html";
|
import { createHtmlPlugin as VitePluginHtml } from "vite-plugin-html";
|
||||||
|
import Icons from "unplugin-icons/vite";
|
||||||
|
|
||||||
export default ({ mode }: { mode: string }) => {
|
export default ({ mode }: { mode: string }) => {
|
||||||
const env = loadEnv(mode, process.cwd(), "");
|
const env = loadEnv(mode, process.cwd(), "");
|
||||||
|
@ -20,6 +21,7 @@ export default ({ mode }: { mode: string }) => {
|
||||||
VueJsx(),
|
VueJsx(),
|
||||||
VueSetupExtend(),
|
VueSetupExtend(),
|
||||||
Compression(),
|
Compression(),
|
||||||
|
Icons({ compiler: "vue3" }),
|
||||||
ViteExternals({
|
ViteExternals({
|
||||||
vue: "Vue",
|
vue: "Vue",
|
||||||
"vue-router": "VueRouter",
|
"vue-router": "VueRouter",
|
||||||
|
|
Loading…
Reference in New Issue