ch3chi-commons-vue 1.2.0 → 1.8.1
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.
- package/package.json +2 -1
- package/src/api/ApiService.ts +869 -0
- package/src/auth/AuthorizationService.ts +138 -0
- package/src/auth/PermissionDescriptor.ts +99 -0
- package/src/auth/keys.ts +5 -0
- package/src/components/CAlert.vue +188 -0
- package/src/components/CAlertDefine.ts +20 -0
- package/src/components/CBSToast.vue +119 -0
- package/src/components/CGlobalSpinner.vue +84 -0
- package/src/components/CImage.vue +67 -0
- package/src/components/CRowCheckBox.vue +75 -0
- package/src/components/CRowTextInput.vue +27 -0
- package/src/components/CTable.vue +524 -0
- package/src/components/CTableDefine.ts +566 -0
- package/src/components/CTableTD.vue +28 -0
- package/src/components/HasPermission.vue +28 -0
- package/src/components/form/CChangePasswordFormField.vue +146 -0
- package/src/components/form/CCheckBoxFormField.vue +91 -0
- package/src/components/form/CCheckBoxPlatFormField.vue +94 -0
- package/src/components/form/CDateFormField.vue +149 -0
- package/src/components/form/CDateQueryField.vue +111 -0
- package/src/components/form/CDateRangeFormField.vue +138 -0
- package/src/components/form/CFilePickerFormField.vue +471 -0
- package/src/components/form/CRadioFormField.vue +62 -0
- package/src/components/form/CRadioPlatFormField.vue +67 -0
- package/src/components/form/CSelectFormField.vue +175 -0
- package/src/components/form/CTextAreaFormField.vue +84 -0
- package/src/components/form/CTextInputFormField.vue +99 -0
- package/src/components/form/CTinyMCEEditorFormField.vue +99 -0
- package/src/components/form/SCTextInputFormField.vue +129 -0
- package/src/composables/useCheckBoxFormField.ts +126 -0
- package/src/composables/useRadioFormField.ts +106 -0
- package/src/directive/CBootstrapDirective.ts +83 -0
- package/src/directive/CDateFormatterDirective.ts +37 -0
- package/src/directive/CFTurnstileDirective.ts +46 -0
- package/src/directive/CFormDirective.ts +57 -0
- package/src/directive/PermissionDirective.ts +102 -0
- package/src/env.d.ts +19 -0
- package/src/index.ts +83 -0
- package/src/model/BSFieldStyleConfig.ts +349 -0
- package/src/model/BaseDictionary.ts +86 -0
- package/src/model/BaseFormDataModel.ts +623 -0
- package/src/model/BaseListViewModel.ts +392 -0
- package/src/model/CBSModalViewModel.ts +91 -0
- package/src/model/CFileDataModel.ts +181 -0
- package/src/model/CImageViewModel.ts +34 -0
- package/src/model/CMenuItem.ts +199 -0
- package/src/model/EmailReceiverDataModel.ts +149 -0
- package/src/model/EmptyDataModel.ts +25 -0
- package/src/model/FormOptions.ts +112 -0
- package/src/model/LoginDataModel.ts +51 -0
- package/src/model/PasswordDataModel.ts +70 -0
- package/src/model/QueryParameter.ts +310 -0
- package/src/model/SessionUser.ts +110 -0
- package/src/model/ShowMessageDataModel.ts +69 -0
- package/src/model/TokenUser.ts +157 -0
- package/src/stores/FormDataStore.ts +73 -0
- package/src/stores/ViewStore.ts +701 -0
- package/src/stores/VueSessionStoreInstaller.ts +22 -0
- package/src/types/turnstile.d.ts +8 -0
- package/src/utils/CToolUtils.ts +133 -0
- package/dist/api/ApiService.d.ts +0 -233
- package/dist/auth/AuthorizationService.d.ts +0 -56
- package/dist/auth/PermissionDescriptor.d.ts +0 -37
- package/dist/components/CAlert.vue.d.ts +0 -17
- package/dist/components/CAlertDefine.d.ts +0 -14
- package/dist/components/CBSToast.vue.d.ts +0 -6
- package/dist/components/CGlobalSpinner.vue.d.ts +0 -13
- package/dist/components/CRowCheckBox.vue.d.ts +0 -14
- package/dist/components/CRowTextInput.vue.d.ts +0 -10
- package/dist/components/CTable.vue.d.ts +0 -24
- package/dist/components/CTableDefine.d.ts +0 -201
- package/dist/components/CTableTD.vue.d.ts +0 -7
- package/dist/components/form/CChangePasswordFormField.vue.d.ts +0 -14
- package/dist/components/form/CCheckBoxFormField.vue.d.ts +0 -30
- package/dist/components/form/CDateFormField.vue.d.ts +0 -17
- package/dist/components/form/CDateQueryField.vue.d.ts +0 -16
- package/dist/components/form/CDateRangeFormField.vue.d.ts +0 -17
- package/dist/components/form/CFilePickerFormField.vue.d.ts +0 -28
- package/dist/components/form/CRadioFormField.vue.d.ts +0 -30
- package/dist/components/form/CSelectFormField.vue.d.ts +0 -18
- package/dist/components/form/CTextAreaFormField.vue.d.ts +0 -16
- package/dist/components/form/CTextInputFormField.vue.d.ts +0 -22
- package/dist/directive/CBootstrapDirective.d.ts +0 -17
- package/dist/directive/CDateFormatterDirective.d.ts +0 -10
- package/dist/directive/CFTurnstileDirective.d.ts +0 -15
- package/dist/directive/CFormDirective.d.ts +0 -9
- package/dist/directive/PermissionDirective.d.ts +0 -15
- package/dist/index.cjs.js +0 -19103
- package/dist/index.d.ts +0 -45
- package/dist/index.es.js +0 -19086
- package/dist/model/BSFieldStyleConfig.d.ts +0 -121
- package/dist/model/BaseDictionary.d.ts +0 -34
- package/dist/model/BaseFormDataModel.d.ts +0 -199
- package/dist/model/BaseListViewModel.d.ts +0 -165
- package/dist/model/CBSModalViewModel.d.ts +0 -44
- package/dist/model/CFileDataModel.d.ts +0 -74
- package/dist/model/CImageViewModel.d.ts +0 -8
- package/dist/model/CMenuItem.d.ts +0 -86
- package/dist/model/EmailReceiverDataModel.d.ts +0 -57
- package/dist/model/EmptyDataModel.d.ts +0 -7
- package/dist/model/FormOptions.d.ts +0 -60
- package/dist/model/LoginDataModel.d.ts +0 -12
- package/dist/model/PasswordDataModel.d.ts +0 -15
- package/dist/model/QueryParameter.d.ts +0 -92
- package/dist/model/SessionUser.d.ts +0 -45
- package/dist/model/ShowMessageDataModel.d.ts +0 -44
- package/dist/model/TokenUser.d.ts +0 -50
- package/dist/stores/FormDataStore.d.ts +0 -31
- package/dist/stores/ViewStore.d.ts +0 -349
- package/dist/style.css +0 -223
- package/dist/utils/CToolUtils.d.ts +0 -53
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import {CMenuItem, MenuItem} from "../model/CMenuItem";
|
|
2
|
+
import _ from "lodash";
|
|
3
|
+
import {PermissionDescriptor} from "./PermissionDescriptor";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 提供選單項目的參數介面
|
|
7
|
+
*/
|
|
8
|
+
type ProvideMenuByPermissionParams = {
|
|
9
|
+
roleCode: string;
|
|
10
|
+
permissions: string[];
|
|
11
|
+
picker?: (roleCode: string) => MenuItem[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* AuthorizationService 類別負責處理權限相關的邏輯
|
|
16
|
+
*/
|
|
17
|
+
export class AuthorizationService {
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 角色權限映射表
|
|
21
|
+
* 定義角色對應多個權限項目
|
|
22
|
+
* @private
|
|
23
|
+
*/
|
|
24
|
+
private static _rolePermissionMap: Record<string, string[]> = {};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 選單定義映射表
|
|
28
|
+
* 定義不同角色對應的選單結構
|
|
29
|
+
* @private
|
|
30
|
+
*/
|
|
31
|
+
private static _menuDefineMap: Record<string, MenuItem[]> = {};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 設定角色權限映射
|
|
35
|
+
* @param map 角色權限映射物件
|
|
36
|
+
*/
|
|
37
|
+
static set rolePermissionMap(map: Record<string, string[]>) {
|
|
38
|
+
this._rolePermissionMap = map;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 設定選單定義映射
|
|
43
|
+
* @param map
|
|
44
|
+
*/
|
|
45
|
+
static set menuDefineMap(map: Record<string, MenuItem[]>) {
|
|
46
|
+
this._menuDefineMap = map;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 配置角色與權限關係,以及選單定義映射
|
|
51
|
+
* @param config 配置物件
|
|
52
|
+
*/
|
|
53
|
+
static configure(config: {
|
|
54
|
+
rolePermissionMap?: Record<string, string[]>;
|
|
55
|
+
menuDefineMap: Record<string, MenuItem[]>;
|
|
56
|
+
}) {
|
|
57
|
+
AuthorizationService.rolePermissionMap = config.rolePermissionMap || {};
|
|
58
|
+
AuthorizationService.menuDefineMap = config.menuDefineMap;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 檢查角色列表是否包含所需的權限
|
|
63
|
+
* @param roleCodes
|
|
64
|
+
* @param requiredPermission
|
|
65
|
+
*/
|
|
66
|
+
static hasPermissionByRole(roleCodes: string[] , requiredPermission: string): boolean {
|
|
67
|
+
for(const role of roleCodes) {
|
|
68
|
+
const permissions = AuthorizationService._rolePermissionMap[role];
|
|
69
|
+
if(permissions && permissions.includes(requiredPermission)) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 根據角色列表提供選單項目
|
|
78
|
+
* @param param 配置參數
|
|
79
|
+
*/
|
|
80
|
+
static provideMenuByPermission(param: ProvideMenuByPermissionParams): CMenuItem[] {
|
|
81
|
+
|
|
82
|
+
// 檢查用戶是否具有訪問特定權限的輔助函數,這個方法比對字串或字串陣列
|
|
83
|
+
const checkPermissionAccess = (itemPermission: string | string[], userPermissions: string[]): boolean => {
|
|
84
|
+
if (Array.isArray(itemPermission)) {
|
|
85
|
+
return itemPermission.some(perm => userPermissions.includes(perm));
|
|
86
|
+
}
|
|
87
|
+
return userPermissions.includes(itemPermission);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// 檢查用戶是否具有訪問特定權限的輔助函數,使用 PermissionDescriptor 物件
|
|
91
|
+
const checkPermissionDescriptorAccess = (descriptors?: PermissionDescriptor[], userPermissions: string[] = []): boolean => {
|
|
92
|
+
if(!descriptors || descriptors.length === 0) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
for(const descriptor of descriptors) {
|
|
96
|
+
if(descriptor.checkPermission(userPermissions)) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 過濾選單,僅保留用戶有權限訪問的選單項目
|
|
104
|
+
const filterMenuItems = (items: MenuItem[]): CMenuItem[] => {
|
|
105
|
+
return items.reduce((acc: CMenuItem[], item: MenuItem) => {
|
|
106
|
+
// 轉換為 CMenuItem
|
|
107
|
+
const menuItem = CMenuItem.parse(item);
|
|
108
|
+
// 如果有子選單,則遞迴過濾子選單
|
|
109
|
+
if (item.children) {
|
|
110
|
+
const filteredChildren = filterMenuItems(item.children);
|
|
111
|
+
if(filteredChildren.length === 0) {
|
|
112
|
+
return acc; // 如果沒有子選單,且本身沒有定義權限,則跳過此選單項目
|
|
113
|
+
}
|
|
114
|
+
menuItem.children = filteredChildren;
|
|
115
|
+
}
|
|
116
|
+
// 用戶權限中是否包含其中一個權限
|
|
117
|
+
const hasDescriptorAccess = checkPermissionDescriptorAccess(menuItem.permissionDescriptors, param.permissions);
|
|
118
|
+
// 如果沒有子選單,且用戶沒有訪問權限,則跳過此選單項目
|
|
119
|
+
if(menuItem.children?.length === 0 && !hasDescriptorAccess) {
|
|
120
|
+
return acc; // 如果沒有訪問權限,跳過此選單項目
|
|
121
|
+
}
|
|
122
|
+
acc.push(menuItem);
|
|
123
|
+
return acc;
|
|
124
|
+
}, []);
|
|
125
|
+
};
|
|
126
|
+
// 根據 roleCode 決定要使用那個 menuDefine
|
|
127
|
+
let menuDefine: MenuItem[];
|
|
128
|
+
if(param.picker) {
|
|
129
|
+
menuDefine = param.picker(param.roleCode);
|
|
130
|
+
} else {
|
|
131
|
+
// 使用預設的選單定義映射,抓 _menuDefineMap 第一個項目當作預設值
|
|
132
|
+
menuDefine = _.values(AuthorizationService._menuDefineMap || {})[0] || [];
|
|
133
|
+
}
|
|
134
|
+
// 回傳過濾後的選單
|
|
135
|
+
return filterMenuItems(menuDefine);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 權限:操作字串列舉
|
|
3
|
+
*/
|
|
4
|
+
export enum PermissionAction {
|
|
5
|
+
NONE = 'NONE', // 無操作
|
|
6
|
+
SIGN_IN = 'SIGN_IN', // 登入
|
|
7
|
+
SIGN_OUT = 'SIGN_OUT', // 登出
|
|
8
|
+
FORGOT_PASSWORD = 'FORGOT_PASSWORD', // 忘記密碼
|
|
9
|
+
RESET_PASSWORD = 'RESET_PASSWORD', // 重設個人密碼
|
|
10
|
+
CHANGE_PASSWORD = 'CHANGE_PASSWORD', // 更改密碼
|
|
11
|
+
|
|
12
|
+
// --- 一般資料操作 ---
|
|
13
|
+
CREATE = 'CREATE', // 建立資料
|
|
14
|
+
SEARCH = 'SEARCH', // 查詢列表
|
|
15
|
+
READ = 'READ', // 讀取資料
|
|
16
|
+
UPDATE = 'UPDATE', // 更新資料
|
|
17
|
+
DELETE = 'DELETE', // 刪除資料
|
|
18
|
+
EXPORT = 'EXPORT', // 匯出資料
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 權限描述類別
|
|
23
|
+
*/
|
|
24
|
+
export class PermissionDescriptor {
|
|
25
|
+
module: string; // 模組名稱
|
|
26
|
+
action: PermissionAction; // 操作字串(定義)
|
|
27
|
+
supportedActions: PermissionAction[] // 支援的操作字串清單
|
|
28
|
+
|
|
29
|
+
// ~ ----------------------------------------------------------
|
|
30
|
+
// ~ constructor
|
|
31
|
+
|
|
32
|
+
constructor(data: Partial<PermissionDescriptor> | string = {}) {
|
|
33
|
+
if(typeof data === 'string') {
|
|
34
|
+
const [module, action] = data.split(':');
|
|
35
|
+
data = {
|
|
36
|
+
module: module,
|
|
37
|
+
action: action as PermissionAction
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
this.module = data.module || '';
|
|
41
|
+
this.action = data.action || PermissionAction.NONE;
|
|
42
|
+
// 根據 action 產生支援的操作字串清單
|
|
43
|
+
this.supportedActions = [this.action];
|
|
44
|
+
switch(this.action) {
|
|
45
|
+
case PermissionAction.CREATE:
|
|
46
|
+
case PermissionAction.SEARCH:
|
|
47
|
+
case PermissionAction.UPDATE:
|
|
48
|
+
case PermissionAction.DELETE:
|
|
49
|
+
this.supportedActions.push(PermissionAction.READ);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ~ ----------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 檢查是否屬於同一個 module
|
|
58
|
+
* @param toCheckPermissions
|
|
59
|
+
*/
|
|
60
|
+
checkModule(toCheckPermissions: string[]): boolean {
|
|
61
|
+
// 檢查是否有任何權限字串屬於同一個 module
|
|
62
|
+
for(const perm of toCheckPermissions) {
|
|
63
|
+
const [module, action] = perm.split(':');
|
|
64
|
+
if(module === this.module) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 檢查是否具有指定的權限
|
|
73
|
+
* @param toCheckPermissions
|
|
74
|
+
*/
|
|
75
|
+
checkPermission(toCheckPermissions: string[]): boolean {
|
|
76
|
+
// 先過濾,找出同一個 module 的權限字串
|
|
77
|
+
const matchModuleArr = toCheckPermissions.filter(item => {
|
|
78
|
+
const [module, action] = item.split(':');
|
|
79
|
+
return module === this.module;
|
|
80
|
+
});
|
|
81
|
+
// 再檢查 action 是否存在於過濾後的清單中
|
|
82
|
+
for(const action of this.supportedActions) {
|
|
83
|
+
const permissionString = `${this.module}:${action}`;
|
|
84
|
+
if(matchModuleArr.includes(permissionString)) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
get permissionString(): string {
|
|
92
|
+
return `${this.module}:${this.action}`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 防止 Vite/rollup tree-shake 掉 type export
|
|
97
|
+
export const __PermissionStringDefine__ = null as unknown as
|
|
98
|
+
PermissionAction;
|
|
99
|
+
|
package/src/auth/keys.ts
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {voidFunction} from "../utils/CToolUtils";
|
|
3
|
+
import {onMounted, ref, computed, reactive} from "vue";
|
|
4
|
+
import {CAlertModalData, CAlertModalType} from "./CAlertDefine";
|
|
5
|
+
import {v4 as uuidv4} from 'uuid';
|
|
6
|
+
import {Modal} from "bootstrap";
|
|
7
|
+
import _ from 'lodash';
|
|
8
|
+
|
|
9
|
+
// ~ ----------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
const props = defineProps<{
|
|
12
|
+
id?: string;
|
|
13
|
+
}>();
|
|
14
|
+
|
|
15
|
+
// ----- attribute -----
|
|
16
|
+
const componentId = props.id || `bs-modal-${uuidv4()}`;
|
|
17
|
+
const titleLabelId = `${componentId}-title`;
|
|
18
|
+
const modalInstance = ref<Modal | null>(null);
|
|
19
|
+
const isModalOpen = ref(false);
|
|
20
|
+
const ariaHiddenText = computed(() => {
|
|
21
|
+
return isModalOpen.value ? 'false' : 'true';
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const viewModel = reactive<{
|
|
25
|
+
type: CAlertModalType | string;
|
|
26
|
+
showTitle: string;
|
|
27
|
+
showContent: string;
|
|
28
|
+
okButtonText: string;
|
|
29
|
+
cancelButtonText: string;
|
|
30
|
+
okCallback: (() => void);
|
|
31
|
+
cancelCallback: (() => void) | null;
|
|
32
|
+
}>({
|
|
33
|
+
type: CAlertModalType.Info, // 用於區分不同類型的 Modal, 例如 'alert', 'confirm' 等
|
|
34
|
+
showTitle: '訊息',
|
|
35
|
+
showContent: '',
|
|
36
|
+
okButtonText: '確定',
|
|
37
|
+
cancelButtonText: '取消',
|
|
38
|
+
okCallback: voidFunction,
|
|
39
|
+
cancelCallback: null,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// 是否顯示取消按鈕
|
|
43
|
+
const hasCancelButton = computed(() => {
|
|
44
|
+
// 根據 type 或 cancelCallback 是否存在來決定是否顯示取消按鈕
|
|
45
|
+
if (viewModel.type === CAlertModalType.Confirm) {
|
|
46
|
+
return true; // 確認類型的 Modal 一定有取消按鈕
|
|
47
|
+
}
|
|
48
|
+
// 如果有 cancelCallback,則顯示取消按鈕
|
|
49
|
+
return !!viewModel.cancelCallback;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// 預設標題根據 type 決定
|
|
53
|
+
const defTitleByType = computed(() => {
|
|
54
|
+
// 根據 type 返回不同的標題
|
|
55
|
+
switch (viewModel.type) {
|
|
56
|
+
case CAlertModalType.Error:
|
|
57
|
+
return '錯誤';
|
|
58
|
+
case CAlertModalType.Alert:
|
|
59
|
+
return '警告';
|
|
60
|
+
case CAlertModalType.Confirm:
|
|
61
|
+
return '確認';
|
|
62
|
+
case CAlertModalType.Info:
|
|
63
|
+
return '訊息';
|
|
64
|
+
default:
|
|
65
|
+
return '訊息';
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const titleExtraClass = computed(() => {
|
|
70
|
+
// 根據 type 返回不同的標題樣式
|
|
71
|
+
const type = viewModel.type.toLowerCase();
|
|
72
|
+
return `c-modal-title-${type}`;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ----- method -----
|
|
76
|
+
function show(data?: CAlertModalData) {
|
|
77
|
+
if (!modalInstance.value) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// 更新標題和內容
|
|
81
|
+
viewModel.type = _.get(data, 'type', CAlertModalType.Info);
|
|
82
|
+
viewModel.showTitle = _.get(data, 'title', defTitleByType.value);
|
|
83
|
+
viewModel.showContent = _.get(data, 'content', '');
|
|
84
|
+
// 如果有 onOk 或 onCancel 方法,則可以在按鈕點擊
|
|
85
|
+
viewModel.okCallback = _.get(data, 'onOk', voidFunction);
|
|
86
|
+
viewModel.cancelCallback = _.get(data, 'onCancel', null);
|
|
87
|
+
// 顯示 Modal
|
|
88
|
+
modalInstance.value.show();
|
|
89
|
+
}
|
|
90
|
+
function hide() {
|
|
91
|
+
if(!modalInstance.value) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
modalInstance.value.hide();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function resetAll() {
|
|
98
|
+
// 重置所有狀態
|
|
99
|
+
viewModel.showTitle = '訊息';
|
|
100
|
+
viewModel.showContent = '';
|
|
101
|
+
viewModel.okButtonText = '確定';
|
|
102
|
+
viewModel.cancelButtonText = '取消';
|
|
103
|
+
viewModel.okCallback = voidFunction;
|
|
104
|
+
viewModel.cancelCallback = null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ----- defineExpose -----
|
|
108
|
+
defineExpose({
|
|
109
|
+
show,
|
|
110
|
+
hide
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
// ----- event -----
|
|
114
|
+
const emit = defineEmits(['c.modal.ok', 'c.modal.cancel']);
|
|
115
|
+
|
|
116
|
+
// 當點擊確定按鈕時
|
|
117
|
+
function doOkClick() {
|
|
118
|
+
if (viewModel.okCallback) {
|
|
119
|
+
viewModel.okCallback();
|
|
120
|
+
}
|
|
121
|
+
emit('c.modal.ok');
|
|
122
|
+
hide();
|
|
123
|
+
}
|
|
124
|
+
function doCancelClick() {
|
|
125
|
+
if (viewModel.cancelCallback) {
|
|
126
|
+
viewModel.cancelCallback();
|
|
127
|
+
}
|
|
128
|
+
emit('c.modal.cancel');
|
|
129
|
+
hide();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ----- LifeCycle -----
|
|
133
|
+
onMounted(() => {
|
|
134
|
+
// 初始化 Bootstrap Modal
|
|
135
|
+
const el = document.getElementById(componentId);
|
|
136
|
+
if (!el) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
modalInstance.value = new Modal(el);
|
|
140
|
+
el.addEventListener('show.bs.modal', () => {
|
|
141
|
+
isModalOpen.value = true;
|
|
142
|
+
});
|
|
143
|
+
el.addEventListener('shown.bs.modal', () => {
|
|
144
|
+
// 取得所有 modal-backdrop
|
|
145
|
+
const backdrops = document.querySelectorAll('.modal-backdrop');
|
|
146
|
+
// 取最後一個(通常是剛產生的)
|
|
147
|
+
const currentBackdrop = backdrops[backdrops.length - 1];
|
|
148
|
+
if (currentBackdrop) {
|
|
149
|
+
// 可對 currentBackdrop 做操作
|
|
150
|
+
currentBackdrop.id = `${componentId}-backdrop`;
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
el.addEventListener('hidden.bs.modal', () => {
|
|
154
|
+
isModalOpen.value = false;
|
|
155
|
+
resetAll()
|
|
156
|
+
});
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
</script>
|
|
160
|
+
|
|
161
|
+
<template>
|
|
162
|
+
<!-- Bootstrap 5 標準 Modal 元件範例 -->
|
|
163
|
+
<div :id="componentId" class="modal fade c-modal-alert c-modal" tabindex="-1"
|
|
164
|
+
:aria-labelledby="titleLabelId"
|
|
165
|
+
:aria-hidden="ariaHiddenText">
|
|
166
|
+
<div class="modal-dialog">
|
|
167
|
+
<div class="modal-content">
|
|
168
|
+
<div class="modal-header" :class="titleExtraClass">
|
|
169
|
+
<h5 class="modal-title" :id="titleLabelId">{{viewModel.showTitle}}</h5>
|
|
170
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
171
|
+
</div>
|
|
172
|
+
<div class="modal-body">
|
|
173
|
+
{{viewModel.showContent}}
|
|
174
|
+
</div>
|
|
175
|
+
<div class="modal-footer">
|
|
176
|
+
<button type="button" class="btn btn-secondary"
|
|
177
|
+
v-if="hasCancelButton"
|
|
178
|
+
@click.prevent.stop="doCancelClick">{{viewModel.cancelButtonText}}</button>
|
|
179
|
+
<button type="button" class="btn btn-primary" @click.prevent.stop="doOkClick">{{viewModel.okButtonText}}</button>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
</template>
|
|
185
|
+
|
|
186
|
+
<style scoped>
|
|
187
|
+
|
|
188
|
+
</style>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface CAlertModalData {
|
|
2
|
+
type?: CAlertModalType | string;
|
|
3
|
+
title?: string;
|
|
4
|
+
content?: string;
|
|
5
|
+
onCancel?: () => void;
|
|
6
|
+
onOk?: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export enum CAlertModalType {
|
|
10
|
+
Error = 'error', // 警告類型
|
|
11
|
+
Alert = 'alert', // 警告類型
|
|
12
|
+
Confirm = 'confirm', // 確認類型
|
|
13
|
+
Info = 'info' // 資訊類型
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
// 防止 Vite/rollup tree-shake 掉 type export
|
|
18
|
+
export const __CAlertModelDefine__ = null as unknown as
|
|
19
|
+
CAlertModalData &
|
|
20
|
+
CAlertModalType;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {reactive} from 'vue';
|
|
3
|
+
import {CAlertModalData, CAlertModalType} from "../components/CAlertDefine";
|
|
4
|
+
import {Toast} from "bootstrap";
|
|
5
|
+
import _ from 'lodash';
|
|
6
|
+
|
|
7
|
+
const dataList: CAlertModalData[] = reactive([]) as CAlertModalData[];
|
|
8
|
+
|
|
9
|
+
function addToast(data: CAlertModalData) {
|
|
10
|
+
data.type = data.type || CAlertModalType.Info;
|
|
11
|
+
dataList.push(data);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function doClose(event: MouseEvent) {
|
|
15
|
+
const target = event.target as HTMLElement;
|
|
16
|
+
const toastEl = target.closest('.toast');
|
|
17
|
+
if (toastEl) {
|
|
18
|
+
const toastInstance = Toast.getInstance(toastEl);
|
|
19
|
+
toastInstance?.hide();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
defineExpose({
|
|
24
|
+
addToast
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function setRef(el: any, dataItem: CAlertModalData) {
|
|
28
|
+
if(_.isNil(el)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
// 當有新的 toast 被添加時,初始化 Bootstrap Toast
|
|
32
|
+
const toastInstance = Toast.getOrCreateInstance(el, {
|
|
33
|
+
animation: true,
|
|
34
|
+
autohide: true,
|
|
35
|
+
delay: 1000 // 自動隱藏延遲時間,單位為毫秒
|
|
36
|
+
});
|
|
37
|
+
el.addEventListener('hide.bs.toast', () => {
|
|
38
|
+
// 從 dataList 中移除已隱藏的 toast
|
|
39
|
+
const index = dataList.indexOf(dataItem);
|
|
40
|
+
if (index !== -1) {
|
|
41
|
+
dataList.splice(index, 1);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
toastInstance.show();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<template>
|
|
50
|
+
<teleport to="body">
|
|
51
|
+
<div class="toast-container position-fixed top-0 end-0 p-3" aria-live="polite" aria-atomic="true">
|
|
52
|
+
<template v-for="(dataItem, index) in dataList" :key="index">
|
|
53
|
+
<div class="toast" :class="`c-toast-${dataItem.type}`" role="alert" aria-live="assertive" aria-atomic="true" :ref="el => setRef(el, dataItem)">
|
|
54
|
+
<div class="toast-header">
|
|
55
|
+
<strong class="me-auto">{{dataItem.title}}</strong>
|
|
56
|
+
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close" @click.prevent="doClose"></button>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="toast-body">
|
|
59
|
+
{{dataItem.content}}
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</template>
|
|
63
|
+
</div>
|
|
64
|
+
</teleport>
|
|
65
|
+
</template>
|
|
66
|
+
|
|
67
|
+
<style scoped>
|
|
68
|
+
.toast.c-toast-error {
|
|
69
|
+
background-color: #ffffff;
|
|
70
|
+
color: #212529;
|
|
71
|
+
border-left: 4px solid #dc3545;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.toast.c-toast-error .toast-header {
|
|
75
|
+
background-color: #f5c6cb;
|
|
76
|
+
color: #721c24;
|
|
77
|
+
border-bottom: 1px solid #f1aeb5;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.toast.c-toast-alert {
|
|
81
|
+
background-color: #ffffff;
|
|
82
|
+
color: #212529;
|
|
83
|
+
border-left: 4px solid #fd7e14;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.toast.c-toast-alert .toast-header {
|
|
87
|
+
background-color: #fde2d3;
|
|
88
|
+
color: #8a4116;
|
|
89
|
+
border-bottom: 1px solid #fde2d3;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.toast.c-toast-info {
|
|
93
|
+
background-color: #ffffff;
|
|
94
|
+
color: #212529;
|
|
95
|
+
border-left: 4px solid #20c997;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.toast.c-toast-info .toast-header {
|
|
99
|
+
background-color: #d4f4e6;
|
|
100
|
+
color: #0f5132;
|
|
101
|
+
border-bottom: 1px solid #d4f4e6;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* 增加一些通用樣式改進 */
|
|
105
|
+
.toast {
|
|
106
|
+
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
|
107
|
+
border-radius: 0.375rem;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.toast-header {
|
|
111
|
+
font-weight: 600;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.toast-body {
|
|
115
|
+
padding: 0.75rem;
|
|
116
|
+
background-color: #ffffff;
|
|
117
|
+
color: #212529;
|
|
118
|
+
}
|
|
119
|
+
</style>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {ref, onUnmounted} from 'vue';
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{
|
|
5
|
+
show?: boolean; // 控制顯示與否
|
|
6
|
+
color?: string; // spinner 顏色
|
|
7
|
+
delayMS?: number; // 延遲顯示的毫秒數,預設 300ms
|
|
8
|
+
isDelay?: boolean; // 是否延遲顯示,預設 false
|
|
9
|
+
}>();
|
|
10
|
+
|
|
11
|
+
const isShow = ref(Boolean(props.show));
|
|
12
|
+
const timer = ref<number | null>(null); // 用於延遲顯示的定時器 ID
|
|
13
|
+
|
|
14
|
+
function showSpinner() {
|
|
15
|
+
if(props.isDelay) {
|
|
16
|
+
showSpinnerDelay();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
// 立即顯示 spinner
|
|
20
|
+
isShow.value = true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function showSpinnerDelay() {
|
|
24
|
+
// 清除任何現有的定時器
|
|
25
|
+
if (timer.value) {
|
|
26
|
+
clearTimeout(timer.value);
|
|
27
|
+
timer.value = null;
|
|
28
|
+
}
|
|
29
|
+
// 設定延遲顯示的定時器
|
|
30
|
+
timer.value = window.setTimeout(() => {
|
|
31
|
+
isShow.value = true;
|
|
32
|
+
}, props.delayMS || 300);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function hide() {
|
|
36
|
+
// 清除定時器並立即隱藏
|
|
37
|
+
if (timer.value) {
|
|
38
|
+
clearTimeout(timer.value);
|
|
39
|
+
timer.value = null;
|
|
40
|
+
}
|
|
41
|
+
isShow.value = false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
onUnmounted(() => {
|
|
45
|
+
if (timer.value) {
|
|
46
|
+
clearTimeout(timer.value);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
defineExpose({
|
|
51
|
+
show: showSpinner,
|
|
52
|
+
hide
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<template>
|
|
58
|
+
<div v-if="isShow" class="c-global-overlay">
|
|
59
|
+
<div class="c-global-spinner">
|
|
60
|
+
<div class="spinner-border" role="status"></div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</template>
|
|
64
|
+
|
|
65
|
+
<style scoped>
|
|
66
|
+
.c-global-overlay {
|
|
67
|
+
position: fixed;
|
|
68
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
69
|
+
background: rgba(44, 44, 44, 0.5);
|
|
70
|
+
z-index: 30000;
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
justify-content: center;
|
|
74
|
+
}
|
|
75
|
+
.c-global-spinner {
|
|
76
|
+
--spinner-color: v-bind(props.color);
|
|
77
|
+
}
|
|
78
|
+
.c-global-spinner .spinner-border {
|
|
79
|
+
width: 3rem;
|
|
80
|
+
height: 3rem;
|
|
81
|
+
color: var(--spinner-color, #E4B445);
|
|
82
|
+
border-width: 0.4em;
|
|
83
|
+
}
|
|
84
|
+
</style>
|