jobdone-shared-files 1.1.12 → 1.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div id="ModalFileRepository" class="modal fade" :class="subModal ? 'sub-modal' : ''" ref="modalDom"
|
|
3
|
+
data-bs-backdrop="static" tabindex="-1" role="dialog" aria-hidden="true">
|
|
4
|
+
<div class="modal-dialog modal-dialog-scrollable" :class="classManager.modalSize" role="document">
|
|
5
|
+
<div class="modal-content">
|
|
6
|
+
<div class="modal-header">
|
|
7
|
+
<h5 class="modal-title"><span class="material-icons icon-18 me-2 text-primary">{{
|
|
8
|
+
optionsAdjust.modalIcon }}</span>{{ optionsAdjust.modalTitle }}</h5>
|
|
9
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="top-header">
|
|
12
|
+
<p>暫存清單</p>
|
|
13
|
+
<template v-if="selectedFilesSet.size > 0">
|
|
14
|
+
<div class="d-flex align-items-center gap-2">
|
|
15
|
+
<small><span>已選擇</span><span class="text-primary mx-1">{{ selectedFilesSet.size
|
|
16
|
+
}}</span><span>個檔案</span></small>
|
|
17
|
+
<button class="btn btn-link btn-sm" @click='clearSelected'><span
|
|
18
|
+
class="material-icons me-1">close</span>取消選取</button>
|
|
19
|
+
</div>
|
|
20
|
+
</template>
|
|
21
|
+
<template v-else>
|
|
22
|
+
<div class="form-check" v-if="optionsAdjust.onlyMyFileCanSwitch">
|
|
23
|
+
<input class="form-check-input" type="checkbox" :true-value="true" :false-value="false"
|
|
24
|
+
:id="`ModalFileRepository_MyFilesOnly_${componentUuid}`" v-model="onlyMyFile"
|
|
25
|
+
@change="reloadRepositoryFiles()" />
|
|
26
|
+
<label class="form-check-label is-unselectable"
|
|
27
|
+
:for="`ModalFileRepository_MyFilesOnly_${componentUuid}`">僅顯示我的檔案</label>
|
|
28
|
+
</div>
|
|
29
|
+
</template>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="modal-body overflow-auto p-0" ref="previewWindow" style="position: relative;">
|
|
32
|
+
<div class="the-month-content m-3" v-for="theMonth in fileListPreview" :key="theMonth.monthKey">
|
|
33
|
+
<p class="the-month-content-header">{{ theMonth.monthLabel }}</p>
|
|
34
|
+
<div class="the-month-content-body">
|
|
35
|
+
<template v-for="(day, dayIdx) in theMonth.days"
|
|
36
|
+
:key="`${theMonth.monthKey}-${day.dayKey}`">
|
|
37
|
+
<div class="the-date-content" v-for="(item, itemIdx) in day.items"
|
|
38
|
+
:key="`${theMonth.monthKey}-${day.dayKey}-${itemIdx}`">
|
|
39
|
+
<p class="the-date-content-header" v-if="itemIdx === 0">{{ day.dayLabel }}</p>
|
|
40
|
+
<div class="the-date-content-body">
|
|
41
|
+
<div class="multimedia-select-content">
|
|
42
|
+
<button class="select-control"
|
|
43
|
+
@click.stop="selectMultimedia(item[optionsAdjust.bindingKey])"
|
|
44
|
+
:class="{ 'is-selected': selectedFilesSet.has(item[optionsAdjust.bindingKey]) }"></button>
|
|
45
|
+
</div>
|
|
46
|
+
<button class="multimedia-preview-content"
|
|
47
|
+
@click="previewImg(item[optionsAdjust.fileUrlKey], item.fileName)">
|
|
48
|
+
<div v-if="item.multimediaList?.length > 0"
|
|
49
|
+
class="the-date-content-multiple-tag">
|
|
50
|
+
</div>
|
|
51
|
+
<img :src="item[optionsAdjust.thumbnailUrlKey] || item[optionsAdjust.fileUrlKey]"
|
|
52
|
+
lazy="loaded" :alt="item.fileName" :title="item.fileName">
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</template>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
<div v-if="fileListPreview.length === 0" class="border-bottom text-center p-4 bg-gray-500">
|
|
60
|
+
<p class="mt-3">暫存媒體庫中沒有任何檔案</p>
|
|
61
|
+
</div>
|
|
62
|
+
<div ref="getNextPagesTarget" style="height: 1rem"></div>
|
|
63
|
+
</div>
|
|
64
|
+
<div class="modal-footer">
|
|
65
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
|
66
|
+
<button type="button" class="btn btn-primary" @click="submitSelected">送出選擇項目 ( {{
|
|
67
|
+
selectedFilesSet.size }}
|
|
68
|
+
)</button>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
<vue-easy-lightbox ref="ezLb" :visible='previewImgAry.length > 0' :imgs="previewImgAry" :index="previewImgIdx"
|
|
74
|
+
@hide="onHide"></vue-easy-lightbox>
|
|
75
|
+
<Loading :is-active="isLoading"></Loading>
|
|
76
|
+
</template>
|
|
77
|
+
<script setup>
|
|
78
|
+
import axios from 'axios';
|
|
79
|
+
import { computed, ref, nextTick, toRaw, onMounted } from "vue";
|
|
80
|
+
import { modalShow, modalHide } from "./common/ModalSetup.js";
|
|
81
|
+
import Loading from "./vueLoadingOverlay.vue";
|
|
82
|
+
import VueEasyLightbox from 'vue-easy-lightbox';
|
|
83
|
+
|
|
84
|
+
const props = defineProps({
|
|
85
|
+
subModal: {
|
|
86
|
+
// 是否為子視窗(預設 false),子視窗會取消 backdrop-filter 效果,且高度略低於一般Modal
|
|
87
|
+
type: Boolean,
|
|
88
|
+
default: false
|
|
89
|
+
},
|
|
90
|
+
projectId: {
|
|
91
|
+
// 專案ID
|
|
92
|
+
type: String,
|
|
93
|
+
required: true
|
|
94
|
+
},
|
|
95
|
+
userId: {
|
|
96
|
+
// 使用者ID
|
|
97
|
+
type: String,
|
|
98
|
+
required: true
|
|
99
|
+
},
|
|
100
|
+
apiUrl: {
|
|
101
|
+
// 取得檔案列表的 API URL
|
|
102
|
+
type: String,
|
|
103
|
+
required: true
|
|
104
|
+
},
|
|
105
|
+
options: {
|
|
106
|
+
// 額外參數設定
|
|
107
|
+
// {
|
|
108
|
+
// modalSize: 'lg' // Modal的尺寸。可選值:sm, '', lg, xl。預設 'lg',
|
|
109
|
+
// modalTitle: '暫存媒體選擇器' // Modal的標題。預設 '暫存媒體選擇器'
|
|
110
|
+
// modalIcon: 'inventory_2', // Modal的圖示。預設 'inventory_2'
|
|
111
|
+
// onlyMyFileCanSwitch: true, // 是否顯示「僅顯示我的檔案」切換選項。預設 true
|
|
112
|
+
// onlyMyFileDefaultValue: true, // 「僅顯示我的檔案」預設選項。預設 true
|
|
113
|
+
// bindingKey: 'uid', // 選項唯一值的keyName。預設 'uid'
|
|
114
|
+
// createAtKey: 'createAt' // 檔案建立時間的keyName。預設 'createAt'
|
|
115
|
+
// fileUrlKey: 'fileUrl', // 檔案URL的keyName。預設'fileUrl'
|
|
116
|
+
// thumbnailUrlKey: 'thumbnailUrl', // 檔案縮圖URL的keyName。預設'thumbnailUrl'
|
|
117
|
+
// }
|
|
118
|
+
type: Object,
|
|
119
|
+
default: () => ({})
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
const emit = defineEmits([
|
|
123
|
+
'submitSelected'
|
|
124
|
+
])
|
|
125
|
+
|
|
126
|
+
const componentUuid = crypto.randomUUID()
|
|
127
|
+
const modalDom = ref(null)
|
|
128
|
+
const isLoading = ref(false)
|
|
129
|
+
|
|
130
|
+
// 參數設定
|
|
131
|
+
const optionConfigs = {
|
|
132
|
+
bindingKey: {
|
|
133
|
+
validate: (value) => typeof value === 'string' && value.length > 0,
|
|
134
|
+
defaultValue: 'uid'
|
|
135
|
+
},
|
|
136
|
+
createAtKey: {
|
|
137
|
+
validate: (value) => typeof value === 'string' && value.length > 0,
|
|
138
|
+
defaultValue: 'createAt'
|
|
139
|
+
},
|
|
140
|
+
fileUrlKey: {
|
|
141
|
+
validate: (value) => typeof value === 'string' && value.length > 0,
|
|
142
|
+
defaultValue: 'fileUrl'
|
|
143
|
+
},
|
|
144
|
+
thumbnailUrlKey: {
|
|
145
|
+
validate: (value) => typeof value === 'string' && value.length > 0,
|
|
146
|
+
defaultValue: 'thumbnailUrl'
|
|
147
|
+
},
|
|
148
|
+
modalSize: {
|
|
149
|
+
validate: (value) => typeof value === 'string' && ['sm', 'md', 'lg', 'xl'].includes(value),
|
|
150
|
+
defaultValue: 'lg' // 可選值:sm, '', lg, xl
|
|
151
|
+
},
|
|
152
|
+
modalTitle: {
|
|
153
|
+
validate: (value) => typeof value === 'string' && value.length > 0,
|
|
154
|
+
defaultValue: '暫存媒體選擇器'
|
|
155
|
+
},
|
|
156
|
+
modalIcon: {
|
|
157
|
+
validate: (value) => typeof value === 'string' && value.length > 0,
|
|
158
|
+
defaultValue: 'inventory_2'
|
|
159
|
+
},
|
|
160
|
+
onlyMyFileCanSwitch: {
|
|
161
|
+
validate: (value) => typeof value === 'boolean',
|
|
162
|
+
defaultValue: true // 是否顯示「僅顯示我的檔案」切換選項
|
|
163
|
+
},
|
|
164
|
+
onlyMyFileDefaultValue: {
|
|
165
|
+
validate: (value) => typeof value === 'boolean',
|
|
166
|
+
defaultValue: true // 「僅顯示我的檔案」預設選項。預設 true
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const optionDefaults = Object.fromEntries(
|
|
170
|
+
Object.entries(optionConfigs).map(([key, config]) => [key, config.defaultValue])
|
|
171
|
+
)
|
|
172
|
+
const optionsAdjust = computed(() => {
|
|
173
|
+
const merged = { ...optionDefaults }
|
|
174
|
+
const incomingOptions = props.options || {}
|
|
175
|
+
for (const [key, value] of Object.entries(incomingOptions)) {
|
|
176
|
+
const config = optionConfigs[key]
|
|
177
|
+
if (!config) {
|
|
178
|
+
console.warn(`ModalFileRepositorySelector: 未知參數 ${key}`)
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
if (!config.validate(value)) {
|
|
182
|
+
console.warn(`ModalFileRepositorySelector: 參數 ${key} 的型態錯誤,將使用預設值 ${config.defaultValue}`)
|
|
183
|
+
continue
|
|
184
|
+
}
|
|
185
|
+
merged[key] = value
|
|
186
|
+
}
|
|
187
|
+
return merged
|
|
188
|
+
})
|
|
189
|
+
const classManager = computed(() => {
|
|
190
|
+
return {
|
|
191
|
+
modalSize: optionsAdjust.value.modalSize ? `modal-${optionsAdjust.value.modalSize}` : `modal-${optionDefaults.modalSize}`
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
const ezLb = ref(null)
|
|
196
|
+
const previewImgAry = ref([])
|
|
197
|
+
const previewImgIdx = ref(0)
|
|
198
|
+
const previewImg = (url, title = '') => { previewImgAry.value = [{ src: url, title: title }] }
|
|
199
|
+
const onHide = () => { previewImgIdx.value = 0; previewImgAry.value = [] }
|
|
200
|
+
|
|
201
|
+
const debounce = (func, delay = 200) => {
|
|
202
|
+
let timeout;
|
|
203
|
+
|
|
204
|
+
return function (...args) {
|
|
205
|
+
const context = this;
|
|
206
|
+
clearTimeout(timeout); // 清除上一次的計時器
|
|
207
|
+
timeout = setTimeout(() => func.apply(context, args), delay);
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
// 滾動讀取
|
|
213
|
+
let observer = null
|
|
214
|
+
const previewWindow = ref(null)
|
|
215
|
+
const getNextPagesTarget = ref(null)
|
|
216
|
+
const checkNeedLoadMore = () => { // 檢查是否需要繼續載入(如果內容高度不足以觸發滾動)
|
|
217
|
+
if (getNextPagesTarget.value && getNextPagesTarget.value.getBoundingClientRect().top <= window.innerHeight) {
|
|
218
|
+
getRepositoryFiles()
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 開啟光箱
|
|
223
|
+
const show = async (selectedFilesUid) => {
|
|
224
|
+
modalShow(modalDom.value, afterCloseModal)
|
|
225
|
+
if (selectedFilesUid && selectedFilesUid.length > 0) {
|
|
226
|
+
selectedFilesSet.value = new Set(selectedFilesUid)
|
|
227
|
+
}
|
|
228
|
+
if (!observer) {
|
|
229
|
+
observer = new IntersectionObserver((entries) => {
|
|
230
|
+
entries.forEach(entry => {
|
|
231
|
+
if (entry.isIntersecting && (getNextPagesTarget.value && getNextPagesTarget.value.getBoundingClientRect().top <= window.innerHeight)) {
|
|
232
|
+
getRepositoryFiles()
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}, {
|
|
236
|
+
root: previewWindow.value,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
observer.observe(getNextPagesTarget.value);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 關閉光箱時的處理
|
|
243
|
+
const afterCloseModal = () => {
|
|
244
|
+
observer.disconnect()
|
|
245
|
+
selectedFilesSet.value.clear()
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 載入檔案
|
|
249
|
+
const repositoryFiles = ref([])
|
|
250
|
+
const fileListPreview = computed(() => { // 預覽用陣列
|
|
251
|
+
const flatMap = repositoryFiles.value.flatMap(pageGroups => pageGroups.data.map(item => ({ ...item, page: pageGroups.page })))
|
|
252
|
+
|
|
253
|
+
// 先按日期排序(最新的在前)
|
|
254
|
+
const sortedItems = flatMap.sort((a, b) => new Date(b[optionsAdjust.value.createAtKey]) - new Date(a[optionsAdjust.value.createAtKey]))
|
|
255
|
+
|
|
256
|
+
// 分組處理
|
|
257
|
+
const groupedData = []
|
|
258
|
+
const monthGroupMap = new Map() // 用 Map 來快速查找月份群組
|
|
259
|
+
const dayGroupMap = new Map() // 用 Map 來快速查找日期群組
|
|
260
|
+
|
|
261
|
+
sortedItems.forEach(item => {
|
|
262
|
+
const date = new Date(item[[optionsAdjust.value.createAtKey]])
|
|
263
|
+
const year = date.getFullYear()
|
|
264
|
+
const month = date.getMonth() + 1
|
|
265
|
+
const day = date.getDate()
|
|
266
|
+
|
|
267
|
+
const monthKey = `${year}-${month.toString().padStart(2, '0')}`
|
|
268
|
+
const dayKey = `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`
|
|
269
|
+
|
|
270
|
+
// 檢查是否需要新增月份群組
|
|
271
|
+
let currentMonthGroup = monthGroupMap.get(monthKey)
|
|
272
|
+
if (!currentMonthGroup) {
|
|
273
|
+
currentMonthGroup = {
|
|
274
|
+
monthKey,
|
|
275
|
+
monthLabel: `${year}年${month}月`,
|
|
276
|
+
days: []
|
|
277
|
+
}
|
|
278
|
+
monthGroupMap.set(monthKey, currentMonthGroup)
|
|
279
|
+
groupedData.push(currentMonthGroup)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// 檢查是否需要新增日期群組
|
|
283
|
+
let currentDayGroup = dayGroupMap.get(dayKey)
|
|
284
|
+
if (!currentDayGroup) {
|
|
285
|
+
currentDayGroup = {
|
|
286
|
+
dayKey,
|
|
287
|
+
dayLabel: `${month}月${day}日`,
|
|
288
|
+
items: []
|
|
289
|
+
}
|
|
290
|
+
dayGroupMap.set(dayKey, currentDayGroup)
|
|
291
|
+
currentMonthGroup.days.push(currentDayGroup)
|
|
292
|
+
}
|
|
293
|
+
// 加入項目到日期群組
|
|
294
|
+
currentDayGroup.items.push(item)
|
|
295
|
+
})
|
|
296
|
+
return groupedData
|
|
297
|
+
})
|
|
298
|
+
const onlyMyFile = ref(true)
|
|
299
|
+
const repositoryFilesPagination = ref({
|
|
300
|
+
pagesToLoad: new Set([1]), // 應載入的頁數
|
|
301
|
+
totalPages: undefined,
|
|
302
|
+
size: 20
|
|
303
|
+
})
|
|
304
|
+
const getRepositoryFiles = async () => {
|
|
305
|
+
isLoading.value = true
|
|
306
|
+
try {
|
|
307
|
+
const fileListPages = new Set(repositoryFiles.value.map(pg => pg.page)) // 已載入頁數
|
|
308
|
+
let needLoadPages = Array.from(repositoryFilesPagination.value.pagesToLoad).filter(pg => !fileListPages.has(pg)) // 需要載入的頁數
|
|
309
|
+
if (!(repositoryFilesPagination.value.totalPages === undefined)) {
|
|
310
|
+
needLoadPages = needLoadPages.filter(pg => pg <= repositoryFilesPagination.value.totalPages) // 過濾超出總頁數的
|
|
311
|
+
}
|
|
312
|
+
if (needLoadPages.length < 1) return // 無需載入,跳出迴圈
|
|
313
|
+
|
|
314
|
+
const getPromise = []
|
|
315
|
+
needLoadPages.forEach(page => {
|
|
316
|
+
getPromise.push(axios.get(props.apiUrl, {
|
|
317
|
+
params: {
|
|
318
|
+
userId: props.userId,
|
|
319
|
+
projectId: props.projectId,
|
|
320
|
+
onlyMyFile: onlyMyFile.value,
|
|
321
|
+
page: page,
|
|
322
|
+
limit: repositoryFilesPagination.value.size
|
|
323
|
+
}
|
|
324
|
+
}))
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
const pageResults = await Promise.allSettled(getPromise)
|
|
328
|
+
pageResults.forEach((result) => {
|
|
329
|
+
console.log(result)
|
|
330
|
+
if (result.status === 'rejected') {
|
|
331
|
+
throw new Error('載入檔案列表失敗')
|
|
332
|
+
}
|
|
333
|
+
if (!result.value?.headers?.["x-pagination"]) {
|
|
334
|
+
throw new Error('分頁資訊遺失')
|
|
335
|
+
}
|
|
336
|
+
if (result.status === 'fulfilled') {
|
|
337
|
+
const resPagination = JSON.parse(result.value.headers["x-pagination"])
|
|
338
|
+
repositoryFiles.value = repositoryFiles.value.filter(pg => pg.page !== resPagination.CurrentPage) // 移除重複頁數
|
|
339
|
+
repositoryFiles.value.push({ data: result.value.data, page: resPagination.CurrentPage });
|
|
340
|
+
repositoryFilesPagination.value.totalPages = resPagination.TotalPages;
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const nextPage = repositoryFilesPagination.value.pagesToLoad.size > 0
|
|
345
|
+
? Math.max(...repositoryFilesPagination.value.pagesToLoad) + 1
|
|
346
|
+
: 1
|
|
347
|
+
if (repositoryFilesPagination.value.totalPages && nextPage > repositoryFilesPagination.value.totalPages) return // 已達最後頁,跳出迴圈
|
|
348
|
+
repositoryFilesPagination.value.pagesToLoad.add(nextPage)
|
|
349
|
+
await nextTick()
|
|
350
|
+
checkNeedLoadMore()
|
|
351
|
+
} catch (error) {
|
|
352
|
+
console.log(error)
|
|
353
|
+
} finally {
|
|
354
|
+
isLoading.value = false
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const reloadRepositoryFiles = debounce(async () => {
|
|
358
|
+
repositoryFilesPagination.value = {
|
|
359
|
+
pagesToLoad: new Set([1]),
|
|
360
|
+
totalPages: undefined,
|
|
361
|
+
size: 20
|
|
362
|
+
}
|
|
363
|
+
repositoryFiles.value = []
|
|
364
|
+
await nextTick()
|
|
365
|
+
await getRepositoryFiles()
|
|
366
|
+
}, 200)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
// 選取檔案
|
|
370
|
+
const selectedFilesSet = ref(new Set())
|
|
371
|
+
const selectMultimedia = (multimediaUid) => {
|
|
372
|
+
if (multimediaUid === undefined || multimediaUid === null) return
|
|
373
|
+
if (selectedFilesSet.value.has(multimediaUid)) {
|
|
374
|
+
selectedFilesSet.value.delete(multimediaUid)
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
selectedFilesSet.value.add(multimediaUid)
|
|
378
|
+
}
|
|
379
|
+
const clearSelected = () => {
|
|
380
|
+
selectedFilesSet.value.clear()
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const submitSelected = () => {
|
|
384
|
+
let res = repositoryFiles.value.flatMap(pageGroups => pageGroups.data).filter(item => selectedFilesSet.value.has(item[optionsAdjust.value.bindingKey])) || []
|
|
385
|
+
res = res.reduce((acc, item) => {
|
|
386
|
+
acc.push(toRaw(item))
|
|
387
|
+
return acc
|
|
388
|
+
}, [])
|
|
389
|
+
// console.log(res)
|
|
390
|
+
emit('submitSelected', res)
|
|
391
|
+
modalHide(modalDom.value)
|
|
392
|
+
}
|
|
393
|
+
onMounted(() => {
|
|
394
|
+
onlyMyFile.value = optionsAdjust.value.onlyMyFileDefaultValue
|
|
395
|
+
})
|
|
396
|
+
defineExpose({
|
|
397
|
+
show,
|
|
398
|
+
onlyMyFile
|
|
399
|
+
})
|
|
400
|
+
</script>
|
|
401
|
+
<!-- styleCss請自行在 js 或 scss 引入
|
|
402
|
+
@import "../scss/Helpers/_ModalFileRepository.scss"; -->
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import Modal from 'bootstrap/js/dist/modal.js';
|
|
2
|
+
|
|
3
|
+
/** 移除焦點Function */
|
|
4
|
+
const blurHandler = () => {
|
|
5
|
+
document.activeElement && document.activeElement.blur();
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/** 取得與 DOM 元素關聯的Modal實例,或在未初始化的情況下建立新的Modal實例。
|
|
9
|
+
* @param { HTMLElement } modalDom 光箱的DOM
|
|
10
|
+
*/
|
|
11
|
+
export const getOrCreateInstance = (modalDom) => {
|
|
12
|
+
return Modal.getOrCreateInstance(modalDom);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 開啟光箱
|
|
17
|
+
* @param { HTMLElement } modalDom 光箱的DOM
|
|
18
|
+
* @param { Function } modalHiddenFun 關閉光箱後的回調函數(等待 CSS 轉換完成後執行)
|
|
19
|
+
* @param { Boolean } hidePrevented static = true 時,使用者點選黑色遮罩被阻止關閉光箱時的回調函數
|
|
20
|
+
*/
|
|
21
|
+
export const modalShow = (modalDom, modalHiddenFun, hidePreventedFun) => {
|
|
22
|
+
const removeEventListeners = () => {
|
|
23
|
+
modalDom.removeEventListener('hide.bs.modal', blurHandler);
|
|
24
|
+
modalDom.removeEventListener('hidden.bs.modal', afterCloseModalFun);
|
|
25
|
+
modalDom.removeEventListener('hidePrevented.bs.modal', hidePreventedFun);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const afterCloseModalFun = () => {
|
|
29
|
+
removeEventListeners();
|
|
30
|
+
if (modalHiddenFun && typeof modalHiddenFun === 'function') {
|
|
31
|
+
modalHiddenFun();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// 先移除事件監聽,避免重複添加
|
|
36
|
+
removeEventListeners();
|
|
37
|
+
|
|
38
|
+
modalDom.addEventListener('hide.bs.modal', blurHandler); // 關閉光箱時需移除焦點,避免aria-hidden="true"的元素仍然有焦點
|
|
39
|
+
modalDom.addEventListener('hidden.bs.modal', afterCloseModalFun);
|
|
40
|
+
if (hidePreventedFun && typeof hidePreventedFun === 'function') {
|
|
41
|
+
modalDom.addEventListener('hidePrevented.bs.modal', hidePreventedFun);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
getOrCreateInstance(modalDom).show();
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 手動關閉光箱
|
|
49
|
+
* @param { HTMLElement } modalDom 光箱的DOM
|
|
50
|
+
*/
|
|
51
|
+
export const modalHide = (modalDom, dispose = false) => {
|
|
52
|
+
getOrCreateInstance(modalDom).hide();
|
|
53
|
+
if (dispose) {
|
|
54
|
+
getOrCreateInstance(modalDom).dispose();
|
|
55
|
+
}
|
|
56
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
#ModalFileRepository {
|
|
2
|
+
&.modal.sub-modal {
|
|
3
|
+
backdrop-filter: none;
|
|
4
|
+
background-color: rgba(#000000, 0.1);
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
.modal-content {
|
|
8
|
+
box-shadow: var(--bs-box-shadow-lg);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
>.modal-dialog:not(.modal-fullscreen) {
|
|
12
|
+
max-height: calc(100% - 100px);
|
|
13
|
+
margin-top: 60px;
|
|
14
|
+
margin-bottom: 40px;
|
|
15
|
+
|
|
16
|
+
@each $key,
|
|
17
|
+
$breakpoint in $grid-breakpoints {
|
|
18
|
+
@media (max-width: $breakpoint) {
|
|
19
|
+
&.modal-fullscreen-#{$key}-down {
|
|
20
|
+
max-height: 100%;
|
|
21
|
+
margin-top: 0;
|
|
22
|
+
margin-bottom: 0;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.the-month-content {
|
|
31
|
+
margin-bottom: 2rem;
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
.the-month-content-header {
|
|
35
|
+
font-size: 1.25rem;
|
|
36
|
+
border-bottom: 1px solid var(--gray-300);
|
|
37
|
+
padding-top: .75rem;
|
|
38
|
+
padding-bottom: .5rem;
|
|
39
|
+
margin-bottom: .5rem;
|
|
40
|
+
position: sticky;
|
|
41
|
+
top: 0;
|
|
42
|
+
backdrop-filter: blur(2px);
|
|
43
|
+
background-color: rgba(white, .8);
|
|
44
|
+
z-index: 10;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.the-month-content-body {
|
|
48
|
+
display: grid;
|
|
49
|
+
gap: .25rem;
|
|
50
|
+
grid-template-columns: repeat(auto-fill, minmax(8.6rem, 1fr));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.the-date-content {
|
|
54
|
+
display: flex;
|
|
55
|
+
justify-content: flex-end;
|
|
56
|
+
flex-direction: column;
|
|
57
|
+
position: relative;
|
|
58
|
+
|
|
59
|
+
.the-date-content-header {
|
|
60
|
+
margin-bottom: 0.25rem;
|
|
61
|
+
font-size: 0.875rem;
|
|
62
|
+
display: grid;
|
|
63
|
+
grid-template-columns: 3px auto 1fr;
|
|
64
|
+
gap: 0.5rem;
|
|
65
|
+
line-height: 1.25rem;
|
|
66
|
+
color: var(--gray-800);
|
|
67
|
+
padding-top: 0.5rem;
|
|
68
|
+
padding-bottom: .5rem;
|
|
69
|
+
margin-bottom: auto;
|
|
70
|
+
|
|
71
|
+
&::before {
|
|
72
|
+
display: block;
|
|
73
|
+
content: " ";
|
|
74
|
+
width: 3px;
|
|
75
|
+
background-color: var(--bs-primary);
|
|
76
|
+
border-radius: 99px;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.the-date-content-body {
|
|
81
|
+
position: relative;
|
|
82
|
+
padding: 0;
|
|
83
|
+
transition: .1s ease-out;
|
|
84
|
+
|
|
85
|
+
&:has(.is-selected) {
|
|
86
|
+
border: 3px solid rgba(var(--bs-primary-rgb), 0.8);
|
|
87
|
+
border-radius: calc(var(--bs-border-radius) + 3px);
|
|
88
|
+
background-color: #fff;
|
|
89
|
+
transform: scale(.95);
|
|
90
|
+
|
|
91
|
+
.multimedia-preview-content {
|
|
92
|
+
transform: scale(.95);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.the-date-content-footer {
|
|
98
|
+
opacity: 0;
|
|
99
|
+
position: absolute;
|
|
100
|
+
transition: .3s ease-out;
|
|
101
|
+
bottom: 0;
|
|
102
|
+
z-index: 10;
|
|
103
|
+
padding: 0.5rem;
|
|
104
|
+
display: flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
justify-content: flex-end;
|
|
107
|
+
width: 100%;
|
|
108
|
+
gap: .5rem;
|
|
109
|
+
background: linear-gradient(#00000000, #000000b0);
|
|
110
|
+
border-bottom-left-radius: var(--bs-border-radius);
|
|
111
|
+
border-bottom-right-radius: var(--bs-border-radius);
|
|
112
|
+
|
|
113
|
+
.save-file,
|
|
114
|
+
.more-info {
|
|
115
|
+
width: 1.5rem;
|
|
116
|
+
height: 1.5rem;
|
|
117
|
+
background-color: transparent;
|
|
118
|
+
z-index: 10;
|
|
119
|
+
margin: 0;
|
|
120
|
+
font-size: 0.5rem;
|
|
121
|
+
display: flex;
|
|
122
|
+
border: none;
|
|
123
|
+
align-items: center;
|
|
124
|
+
justify-content: center;
|
|
125
|
+
color: #fff;
|
|
126
|
+
transition: 0.3s ease-out;
|
|
127
|
+
|
|
128
|
+
&::before {
|
|
129
|
+
transition: 0.3s ease-out;
|
|
130
|
+
font-size: 1.25rem;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
&:hover::before {
|
|
134
|
+
color: var(--blue-500);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.save-file {
|
|
139
|
+
&::before {
|
|
140
|
+
content: "file_download";
|
|
141
|
+
font-family: "Material Icons";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
&:hover {
|
|
145
|
+
cursor: pointer;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.more-info {
|
|
150
|
+
&::before {
|
|
151
|
+
content: "info";
|
|
152
|
+
font-family: "Material Icons";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
&:hover {
|
|
156
|
+
cursor: pointer;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
&:hover,
|
|
163
|
+
&.batch-operating {
|
|
164
|
+
|
|
165
|
+
.the-date-content-footer,
|
|
166
|
+
.select-control {
|
|
167
|
+
opacity: 1;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.multimedia-preview-content.is-video::before {
|
|
171
|
+
transform: scale(1.1);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
&:hover {
|
|
176
|
+
cursor: pointer;
|
|
177
|
+
|
|
178
|
+
img {
|
|
179
|
+
transform: scale(1.05);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.multimedia-select-content {
|
|
185
|
+
position: absolute;
|
|
186
|
+
width: 100%;
|
|
187
|
+
width: -webkit-fill-available;
|
|
188
|
+
|
|
189
|
+
.select-control {
|
|
190
|
+
border: 0;
|
|
191
|
+
background-color: transparent;
|
|
192
|
+
top: 0;
|
|
193
|
+
right: 0;
|
|
194
|
+
padding: 0.5rem;
|
|
195
|
+
z-index: 10;
|
|
196
|
+
margin: 0;
|
|
197
|
+
position: absolute;
|
|
198
|
+
user-select: none;
|
|
199
|
+
// opacity: 0;
|
|
200
|
+
transition: opacity 0.2s ease-out;
|
|
201
|
+
|
|
202
|
+
&::before {
|
|
203
|
+
content: "check";
|
|
204
|
+
display: block;
|
|
205
|
+
position: absolute;
|
|
206
|
+
margin: auto;
|
|
207
|
+
color: #fff;
|
|
208
|
+
font-size: 1rem;
|
|
209
|
+
line-height: 1rem;
|
|
210
|
+
opacity: .5;
|
|
211
|
+
font-family: "Material Icons";
|
|
212
|
+
z-index: 10;
|
|
213
|
+
width: 1rem;
|
|
214
|
+
height: 1rem;
|
|
215
|
+
left: calc(50% - 0.5rem);
|
|
216
|
+
top: calc(50% - 0.5rem);
|
|
217
|
+
transform: translateY(-1px);
|
|
218
|
+
transition: opacity 0.2s ease-out;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
&::after {
|
|
222
|
+
opacity: .8;
|
|
223
|
+
border: 2px solid;
|
|
224
|
+
border-color: #ffffff85;
|
|
225
|
+
content: " ";
|
|
226
|
+
display: block;
|
|
227
|
+
width: 1.75rem;
|
|
228
|
+
height: 1.75rem;
|
|
229
|
+
background-color: rgba(#000, 0.2);
|
|
230
|
+
border-radius: var(--bs-border-radius);
|
|
231
|
+
backdrop-filter: blur(2px);
|
|
232
|
+
transition: 0.2s ease-out;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
&:hover {
|
|
236
|
+
cursor: pointer;
|
|
237
|
+
|
|
238
|
+
&::before {
|
|
239
|
+
opacity: 1;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
&::after {
|
|
243
|
+
// background-color: rgba(100, 122, 241, 0);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
&.is-selected {
|
|
248
|
+
opacity: 1;
|
|
249
|
+
|
|
250
|
+
&::before {
|
|
251
|
+
opacity: 1;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
&::after {
|
|
255
|
+
opacity: 1;
|
|
256
|
+
background-color: var(--bs-primary);
|
|
257
|
+
border-color: #fff;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.multimedia-preview-content {
|
|
264
|
+
position: relative;
|
|
265
|
+
display: block;
|
|
266
|
+
border: 0;
|
|
267
|
+
background-color: transparent;
|
|
268
|
+
width: 100%;
|
|
269
|
+
padding-top: 100%;
|
|
270
|
+
overflow: hidden;
|
|
271
|
+
background-color: var(--purple-100);
|
|
272
|
+
border-radius: var(--bs-border-radius);
|
|
273
|
+
|
|
274
|
+
img {
|
|
275
|
+
transition: 0.3s ease-out;
|
|
276
|
+
border-radius: var(--bs-border-radius);
|
|
277
|
+
width: 100%;
|
|
278
|
+
height: 100%;
|
|
279
|
+
object-fit: cover;
|
|
280
|
+
position: absolute;
|
|
281
|
+
top: 0;
|
|
282
|
+
left: 0;
|
|
283
|
+
/* 確保圖片覆蓋整個方形區域 */
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
&.is-video::before {
|
|
289
|
+
position: absolute;
|
|
290
|
+
transition: 0.3s ease-out;
|
|
291
|
+
background-color: rgba(#000, 0.4);
|
|
292
|
+
color: #fff;
|
|
293
|
+
backdrop-filter: blur(2px);
|
|
294
|
+
display: flex;
|
|
295
|
+
justify-content: center;
|
|
296
|
+
align-items: center;
|
|
297
|
+
width: 2.8rem;
|
|
298
|
+
height: 2.8rem;
|
|
299
|
+
top: calc(50% - 1.4rem);
|
|
300
|
+
left: calc(50% - 1.4rem);
|
|
301
|
+
box-shadow: 0 0px 12px 4px #c3c3c396;
|
|
302
|
+
z-index: 100;
|
|
303
|
+
font-family: "Material Icons";
|
|
304
|
+
content: "play_arrow";
|
|
305
|
+
font-size: 1.5rem;
|
|
306
|
+
border-radius: 99px;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.is-unselectable {
|
|
314
|
+
user-select: none;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.top-header {
|
|
318
|
+
display: flex;
|
|
319
|
+
align-items: center;
|
|
320
|
+
border-bottom: 1px solid var(--gray-300);
|
|
321
|
+
background-color: var(--gray-100);
|
|
322
|
+
|
|
323
|
+
>* {
|
|
324
|
+
padding: .5rem 1rem;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
p {
|
|
328
|
+
color: var(--gray-500);
|
|
329
|
+
margin-bottom: 0;
|
|
330
|
+
margin-right: auto;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.the-date-content-multiple-tag {
|
|
335
|
+
position: absolute;
|
|
336
|
+
z-index: 2;
|
|
337
|
+
top: 0;
|
|
338
|
+
left: 0;
|
|
339
|
+
border: none;
|
|
340
|
+
background-color: transparent;
|
|
341
|
+
padding: 0;
|
|
342
|
+
margin: 0;
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
&::before {
|
|
347
|
+
height: 1.875rem;
|
|
348
|
+
width: 2rem;
|
|
349
|
+
content: "insert_link";
|
|
350
|
+
font-family: "Material Icons";
|
|
351
|
+
font-size: 1rem;
|
|
352
|
+
line-height: 1rem;
|
|
353
|
+
display: flex;
|
|
354
|
+
align-items: center;
|
|
355
|
+
justify-content: center;
|
|
356
|
+
background-color: rgba(var(--green-500-rgb), .9);
|
|
357
|
+
color: #fff;
|
|
358
|
+
border-top-left-radius: var(--bs-border-radius);
|
|
359
|
+
border-bottom-right-radius: var(--bs-border-radius);
|
|
360
|
+
top: 0;
|
|
361
|
+
left: 0;
|
|
362
|
+
transition: .1s ease-out;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|