create-pubinfo 2.0.0-beta.3 → 2.0.0-beta.30

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.
Files changed (69) hide show
  1. package/dist/index.js +187 -129
  2. package/package.json +8 -7
  3. package/templates/pubinfo-template/.browserslistrc +5 -0
  4. package/templates/pubinfo-template/.editorconfig +10 -0
  5. package/templates/pubinfo-template/.env +12 -0
  6. package/templates/pubinfo-template/.env.development +6 -0
  7. package/templates/pubinfo-template/.env.production +10 -0
  8. package/templates/pubinfo-template/_gitignore +37 -0
  9. package/templates/pubinfo-template/_npmrc +5 -0
  10. package/templates/pubinfo-template/commitlint.config.js +66 -0
  11. package/templates/pubinfo-template/eslint.config.ts +3 -0
  12. package/templates/pubinfo-template/index.html +47 -0
  13. package/templates/pubinfo-template/openapi.config.ts +33 -0
  14. package/templates/pubinfo-template/package.json +53 -0
  15. package/templates/pubinfo-template/pubinfo.config.ts +9 -0
  16. package/templates/pubinfo-template/public/browser_upgrade/chrome.png +0 -0
  17. package/templates/pubinfo-template/public/browser_upgrade/edge.png +0 -0
  18. package/templates/pubinfo-template/public/browser_upgrade/index.css +49 -0
  19. package/templates/pubinfo-template/public/loading.css +92 -0
  20. package/templates/pubinfo-template/src/App.vue +7 -0
  21. package/templates/pubinfo-template/src/api/modules/auth/index.ts +3 -0
  22. package/templates/pubinfo-template/src/api/modules/auth/renzhengfuwu.ts +145 -0
  23. package/templates/pubinfo-template/src/api/modules/auth/typings.d.ts +97 -0
  24. package/templates/pubinfo-template/src/api/request.ts +124 -0
  25. package/templates/pubinfo-template/src/assets/icons/logo.svg +1 -0
  26. package/templates/pubinfo-template/src/assets/icons/process-management.svg +1 -0
  27. package/templates/pubinfo-template/src/assets/icons/workbench.svg +1 -0
  28. package/templates/pubinfo-template/src/assets/images/login-bg.webp +0 -0
  29. package/templates/pubinfo-template/src/assets/images/login-bg_dark.webp +0 -0
  30. package/templates/pubinfo-template/src/assets/images/login-small.png +0 -0
  31. package/templates/pubinfo-template/src/assets/images/login-small_dark.webp +0 -0
  32. package/templates/pubinfo-template/src/components/UIProvider/index.vue +51 -0
  33. package/templates/pubinfo-template/src/layouts/index.vue +38 -0
  34. package/templates/pubinfo-template/src/main.ts +25 -0
  35. package/templates/pubinfo-template/src/modules/auth.ts +21 -0
  36. package/templates/pubinfo-template/src/modules/rbac.ts +52 -0
  37. package/templates/pubinfo-template/src/routes/index.ts +71 -0
  38. package/templates/pubinfo-template/src/routes/modules/demo/breadcrumb.example.ts +62 -0
  39. package/templates/pubinfo-template/src/routes/modules/demo/link.ts +15 -0
  40. package/templates/pubinfo-template/src/routes/modules/demo/multilevel.menu.example.ts +68 -0
  41. package/templates/pubinfo-template/src/routes/modules/demo/other.page.ts +37 -0
  42. package/templates/pubinfo-template/src/routes/modules/demo/single.ts +14 -0
  43. package/templates/pubinfo-template/src/settings.ts +8 -0
  44. package/templates/pubinfo-template/src/stores/index.ts +2 -0
  45. package/templates/pubinfo-template/src/stores/modules/conter.ts +16 -0
  46. package/templates/pubinfo-template/src/views/demo/breadcrumb_example/detail1.vue +11 -0
  47. package/templates/pubinfo-template/src/views/demo/breadcrumb_example/detail2.vue +11 -0
  48. package/templates/pubinfo-template/src/views/demo/breadcrumb_example/list1.vue +11 -0
  49. package/templates/pubinfo-template/src/views/demo/breadcrumb_example/list2.vue +11 -0
  50. package/templates/pubinfo-template/src/views/demo/multilevel_menu_example/level2/level3/page1.vue +11 -0
  51. package/templates/pubinfo-template/src/views/demo/multilevel_menu_example/level2/level3/page2.vue +11 -0
  52. package/templates/pubinfo-template/src/views/demo/multilevel_menu_example/level2/page.vue +11 -0
  53. package/templates/pubinfo-template/src/views/demo/multilevel_menu_example/page.vue +11 -0
  54. package/templates/pubinfo-template/src/views/demo/other_page/des.vue +13 -0
  55. package/templates/pubinfo-template/src/views/demo/other_page/index.vue +25 -0
  56. package/templates/pubinfo-template/src/views/demo/preview-empty/index.vue +17 -0
  57. package/templates/pubinfo-template/src/views/demo/single/index.vue +13 -0
  58. package/templates/pubinfo-template/src/views/system/index.vue +5 -0
  59. package/templates/pubinfo-template/src/views/system/login/components/LoginForm.vue +68 -0
  60. package/templates/pubinfo-template/src/views/system/login/components/LoginWithPhone.vue +213 -0
  61. package/templates/pubinfo-template/src/views/system/login/components/PasswordLogin.vue +214 -0
  62. package/templates/pubinfo-template/src/views/system/login/components/Savephone.vue +17 -0
  63. package/templates/pubinfo-template/src/views/system/login/components/Useragreement.vue +26 -0
  64. package/templates/pubinfo-template/src/views/system/login/composables.ts +84 -0
  65. package/templates/pubinfo-template/src/views/system/login/index.vue +165 -0
  66. package/templates/pubinfo-template/stylelint.config.js +3 -0
  67. package/templates/pubinfo-template/tsconfig.json +3 -0
  68. package/templates/pubinfo-template/uno.config.ts +17 -0
  69. package/dist/index.cjs +0 -364
@@ -0,0 +1,13 @@
1
+ <script setup lang="ts">
2
+ import { PageMain } from 'pubinfo';
3
+
4
+ defineOptions({
5
+ name: 'Demo',
6
+ });
7
+ </script>
8
+
9
+ <template>
10
+ <PageMain>
11
+ 演示页面,没有任何内容
12
+ </PageMain>
13
+ </template>
@@ -0,0 +1,5 @@
1
+ <template>
2
+ <div class="home">
3
+ 主页
4
+ </div>
5
+ </template>
@@ -0,0 +1,68 @@
1
+ <script setup lang="ts">
2
+ import { useLoginTabs } from '../composables';
3
+
4
+ defineOptions({
5
+ name: 'LoginFrom',
6
+ });
7
+
8
+ const { tabs, initialTab, changeActiveTab } = useLoginTabs();
9
+ </script>
10
+
11
+ <template>
12
+ <div class="login-warp">
13
+ <div class="login-tabs">
14
+ <div
15
+ v-for="(tab, index) in tabs"
16
+ :key="tab.title"
17
+ class="login-tab"
18
+ :class="{ active: initialTab === index }"
19
+ :style="{ width: `${100 / tabs.length}%` }"
20
+ @click="changeActiveTab(index)"
21
+ >
22
+ {{ tab.title }}
23
+ </div>
24
+ </div>
25
+ <div class="login-form">
26
+ <component :is="tabs[initialTab].component" :key="tabs[initialTab].title" :name="tabs[initialTab].title" />
27
+ </div>
28
+ </div>
29
+ </template>
30
+
31
+ <style scoped lang="scss">
32
+ .login-warp {
33
+ display: flex;
34
+ flex-direction: column;
35
+ width: 456px;
36
+ background: transparent;
37
+
38
+ .login-tabs {
39
+ display: flex;
40
+ flex-direction: row;
41
+ overflow: hidden;
42
+ border: 1px solid #e7e8e9;
43
+ border-radius: 4px;
44
+
45
+ .login-tab {
46
+ display: flex;
47
+ flex-direction: row;
48
+ align-items: center;
49
+ justify-content: center;
50
+ height: 40px;
51
+ font-size: 16px;
52
+ color: #8a8e92;
53
+ cursor: pointer;
54
+ border-radius: 4px;
55
+ }
56
+
57
+ .active {
58
+ color: #2f6bff;
59
+ background: #ecf1fd;
60
+ }
61
+ }
62
+
63
+ .login-form {
64
+ position: relative;
65
+ min-height: 350px;
66
+ }
67
+ }
68
+ </style>
@@ -0,0 +1,213 @@
1
+ <script setup lang="ts">
2
+ import type { SelectProps } from 'ant-design-vue';
3
+ import type { FormInstance, Rule } from 'ant-design-vue/es/form';
4
+ import { useToggle } from '@vueuse/core';
5
+ import { message } from 'ant-design-vue';
6
+ import { useCountdown } from '../composables';
7
+ import Savephone from './Savephone.vue';
8
+
9
+ defineOptions({
10
+ name: 'LoginWithPhone',
11
+ });
12
+
13
+ interface FormState {
14
+ phone: string
15
+ verify: string
16
+ checked: false
17
+ }
18
+ const [verifyDisabled, setVerifyDisabled] = useToggle(true);
19
+ const phoneForm = ref<FormInstance>();
20
+ const { count, start, isStart } = useCountdown();
21
+ const regionCode = ref<number>(0);
22
+ const router = useRouter();
23
+ const route = useRoute();
24
+
25
+ const formState = reactive<FormState>({
26
+ phone: '',
27
+ verify: '',
28
+ checked: false,
29
+ });
30
+
31
+ const rules: Record<string, Rule[]> = {
32
+ phone: [
33
+ {
34
+ validator: checkPhoneIsCorrect,
35
+ whitespace: true,
36
+ trigger: 'change',
37
+ },
38
+ ],
39
+ verify: [
40
+ {
41
+ required: true,
42
+ whitespace: true,
43
+ message: '请输入验证码',
44
+ },
45
+ ],
46
+ };
47
+
48
+ const regionCodeOptins: SelectProps['options'] = [
49
+ {
50
+ value: 0,
51
+ label: '+86 (中国大陆)',
52
+ },
53
+ {
54
+ value: 1,
55
+ label: '+852 (香港行政区)',
56
+ },
57
+ {
58
+ value: 2,
59
+ label: '+853 (澳门行政区)',
60
+ },
61
+ {
62
+ value: 3,
63
+ label: '+886 (台湾省)',
64
+ },
65
+ ];
66
+
67
+ async function checkPhoneIsCorrect(_rule: Rule, value: string) {
68
+ function reject(area: string) {
69
+ setVerifyDisabled(true);
70
+ return Promise.reject(new Error(`请输入正确的${area}手机号`));
71
+ }
72
+ if (!value) {
73
+ setVerifyDisabled(true);
74
+ return Promise.reject(new Error(`请输入手机号`));
75
+ }
76
+ switch (regionCode.value) {
77
+ // 中国大陆
78
+ case 0:
79
+ if (!/^1[3-9]\d{9}$/.test(value)) {
80
+ return reject('中国大陆');
81
+ }
82
+ break;
83
+ // 香港特别行政区
84
+ case 1:
85
+ if (!/^([569])\d{7}$/.test(value)) {
86
+ return reject('香港特别行政区');
87
+ }
88
+ break;
89
+ // 澳门特别行政区
90
+ case 2:
91
+ if (!/^6\d{7}$/.test(value)) {
92
+ return reject('澳门特别行政区');
93
+ }
94
+ break;
95
+ // 台湾省
96
+ case 3:
97
+ if (!/^9\d{8}$/.test(value)) {
98
+ return reject('台湾省');
99
+ }
100
+ break;
101
+ default:
102
+ break;
103
+ }
104
+ setVerifyDisabled(false);
105
+ }
106
+
107
+ async function areaChange() {
108
+ if (!formState.phone) {
109
+ return;
110
+ }
111
+ phoneForm.value?.validateFields('phone');
112
+ }
113
+
114
+ async function sendVerifyCode() {
115
+ message.success('验证码已发送,请注意查收');
116
+ start();
117
+ }
118
+
119
+ async function submit() {
120
+ try {
121
+ await phoneForm.value?.validate();
122
+
123
+ // TODO 实现手机号登录逻辑
124
+
125
+ if (formState.checked) {
126
+ localStorage.setItem('pubinfo_web_savedPhone', formState.phone);
127
+ }
128
+ else {
129
+ localStorage.removeItem('pubinfo_web_savedPhone');
130
+ }
131
+ message.success('登录成功');
132
+ router.push(route.query.redirect?.toString() ?? '/');
133
+ }
134
+ catch (error) {
135
+ }
136
+ }
137
+
138
+ onMounted(() => {
139
+ const phone = localStorage.getItem('pubinfo_web_savedPhone');
140
+ if (phone) {
141
+ formState.phone = phone;
142
+ setVerifyDisabled(false);
143
+ formState.checked = true as any;
144
+ }
145
+ });
146
+ </script>
147
+
148
+ <template>
149
+ <div class="phone-form" mt-40px>
150
+ <a-form
151
+ ref="phoneForm"
152
+ :model="formState"
153
+ autocomplete="off"
154
+ :rules="rules"
155
+ @finish="submit"
156
+ >
157
+ <a-form-item name="phone">
158
+ <a-input-group compact>
159
+ <a-form-item-rest>
160
+ <a-select
161
+ v-model:value="regionCode"
162
+ size="large"
163
+ style="width: 35%;"
164
+ :options="regionCodeOptins"
165
+ @change="areaChange"
166
+ />
167
+ </a-form-item-rest>
168
+ <a-input
169
+ v-model:value="formState.phone"
170
+ allow-clear
171
+ size="large"
172
+ placeholder="请输入手机号"
173
+ style="width: 65%;"
174
+ />
175
+ </a-input-group>
176
+ </a-form-item>
177
+
178
+ <a-form-item name="verify">
179
+ <a-input
180
+ v-model:value="formState.verify"
181
+ allow-clear
182
+ size="large"
183
+ placeholder="请输入验证码"
184
+ >
185
+ <template #suffix>
186
+ <a-button
187
+ size="small"
188
+ type="link"
189
+ :disabled="verifyDisabled"
190
+ >
191
+ <span v-if="!isStart" @click="sendVerifyCode">获取短信验证码</span>
192
+ <span v-else text="#8A8E92">{{ count }}秒后重新获取</span>
193
+ </a-button>
194
+ </template>
195
+ </a-input>
196
+ </a-form-item>
197
+ <a-form-item>
198
+ <Savephone v-model:checked="formState.checked" />
199
+ </a-form-item>
200
+ <a-form-item>
201
+ <a-button
202
+ class="h-48px!"
203
+ block
204
+ type="primary"
205
+ html-type="submit"
206
+ :disabled="!formState.checked"
207
+ >
208
+ 登录
209
+ </a-button>
210
+ </a-form-item>
211
+ </a-form>
212
+ </div>
213
+ </template>
@@ -0,0 +1,214 @@
1
+ <script setup lang="ts">
2
+ import type { FormInstance, Rule } from 'ant-design-vue/es/form';
3
+ import type { UnwrapRef } from 'vue';
4
+ import { auth } from '@/modules/auth';
5
+ import { useToggle } from '@vueuse/core';
6
+ import { message, Modal } from 'ant-design-vue';
7
+ import { useUserStore } from 'pubinfo';
8
+ import { RESPONSE_CODE } from 'pubinfo/request';
9
+ import { useCaptchas, useFocusElement } from '../composables';
10
+ import Useragreement from './Useragreement.vue';
11
+
12
+ defineOptions({
13
+ name: 'PasswordLogin',
14
+ });
15
+
16
+ interface FormState {
17
+ account: string
18
+ password: string
19
+ captchas: string
20
+ captchaHash: string
21
+ checked: boolean
22
+ }
23
+
24
+ interface HttpError {
25
+ code: RESPONSE_CODE
26
+ data: any
27
+ hint: string
28
+ msg: string
29
+ success: boolean
30
+ }
31
+
32
+ function isHttpError(error: unknown): error is HttpError {
33
+ return (error as HttpError).code !== undefined;
34
+ }
35
+
36
+ const userStore = useUserStore();
37
+ const { send, captcha, captchaHash } = useCaptchas();
38
+ const router = useRouter();
39
+ const route = useRoute();
40
+ const { focusElementDataId } = useFocusElement();
41
+
42
+ const loginForm = ref<FormInstance | null>(null);
43
+ const formState: UnwrapRef<FormState> = reactive({
44
+ account: '',
45
+ password: '',
46
+ captchas: '',
47
+ checked: true,
48
+ captchaHash,
49
+ });
50
+
51
+ const rules: Record<string, Rule[]> = {
52
+ account: [{ required: true, whitespace: true, message: '请输入账号' }],
53
+ password: [{ required: true, whitespace: true, message: '请输入密码' }],
54
+ captchas: [{ required: true, whitespace: true, message: '请输入验证码' }],
55
+ };
56
+
57
+ const [loading, setLoading] = useToggle(false);
58
+
59
+ async function onSubmit() {
60
+ if (loading.value) {
61
+ return;
62
+ }
63
+ setLoading(true);
64
+ try {
65
+ await loginForm.value?.validate();
66
+ await userStore.login(formState);
67
+ message.success('登录成功');
68
+ router.push(route.query.redirect?.toString() ?? '/');
69
+ }
70
+ catch (error) {
71
+ if (isHttpError(error)) {
72
+ const code = error.code;
73
+ switch (code) {
74
+ case RESPONSE_CODE.LOGINNAME_PASSWORD_WRONG:
75
+ case RESPONSE_CODE.CODE_OVERTIME:
76
+ case RESPONSE_CODE.CODE_WRONG:
77
+ send();
78
+ break;
79
+ case RESPONSE_CODE.CHANGE_INIT_PASSWORD:
80
+ case RESPONSE_CODE.PASSWORD_EXPIRED:
81
+ createChangePasswordModal(error.msg, error?.data?.accessToken);
82
+ break;
83
+ default:
84
+ break;
85
+ }
86
+ }
87
+ }
88
+ finally {
89
+ setLoading(false);
90
+ }
91
+ }
92
+
93
+ function createChangePasswordModal(msg: string, token: string) {
94
+ Modal.confirm({
95
+ title: '提示',
96
+ content() {
97
+ return msg;
98
+ },
99
+ okText: '修改密码',
100
+ cancelText: '暂不登录',
101
+ onOk() {
102
+ router.push({
103
+ name: 'ChangePassword',
104
+ params: {
105
+ changePassWordToken: token,
106
+ },
107
+ });
108
+ },
109
+ });
110
+ }
111
+
112
+ const AUTH_4A_ENABLED = import.meta.env.VITE_AUTH_4A_ENABLED === 'true';
113
+ </script>
114
+
115
+ <template>
116
+ <div class="login-form">
117
+ <AForm ref="loginForm" :model="formState" :rules="rules" @finish="onSubmit">
118
+ <a-form-item name="account">
119
+ <a-input
120
+ v-model:value="formState.account"
121
+ allow-clear
122
+ size="large"
123
+ placeholder="请输入登录账号"
124
+ data-id="account"
125
+ >
126
+ <template #prefix>
127
+ <PubinfoIcon v-if="focusElementDataId === 'account'" name="i-solar-user-bold" color="#1578FF" />
128
+ <PubinfoIcon v-else name="i-solar-user-broken" color="#AFB0B2" />
129
+ </template>
130
+ </a-input>
131
+ </a-form-item>
132
+ <a-form-item name="password">
133
+ <a-input-password
134
+ v-model:value="formState.password"
135
+ allow-clear
136
+ size="large"
137
+ placeholder="请输入登录密码"
138
+ visibility-toggle
139
+ data-id="password"
140
+ >
141
+ <template #prefix>
142
+ <PubinfoIcon v-if="focusElementDataId === 'password'" name="i-solar-lock-password-unlocked-bold" color="#1578FF" />
143
+ <PubinfoIcon v-else name="i-solar-lock-password-unlocked-broken" color="#AFB0B2" />
144
+ </template>
145
+ </a-input-password>
146
+ </a-form-item>
147
+ <a-form-item name="captchas">
148
+ <a-input
149
+ v-model:value="formState.captchas"
150
+ size="large"
151
+ allow-clear
152
+ placeholder="请输入验证码"
153
+ data-id="captchas"
154
+ >
155
+ <template #prefix>
156
+ <PubinfoIcon v-if="focusElementDataId === 'captchas'" name="i-solar-shield-check-bold" color="#1578FF" />
157
+ <PubinfoIcon v-else name="i-solar-shield-check-broken" color="#AFB0B2" />
158
+ </template>
159
+ <template #addonAfter>
160
+ <div
161
+ w-100px
162
+ h-38px
163
+ cursor-pointer
164
+ @click="send"
165
+ >
166
+ <img :src="captcha">
167
+ </div>
168
+ </template>
169
+ </a-input>
170
+ </a-form-item>
171
+ <a-form-item>
172
+ <Useragreement v-model:checked="formState.checked" />
173
+ </a-form-item>
174
+ <!-- 登录 -->
175
+ <a-form-item>
176
+ <div w-full h-48px>
177
+ <a-button
178
+ class="h-48px!"
179
+ type="primary"
180
+ html-type="submit"
181
+ block
182
+ :disabled="!formState.checked"
183
+ :loading="loading"
184
+ >
185
+ 登录
186
+ </a-button>
187
+ </div>
188
+ <div v-if="AUTH_4A_ENABLED" w-full h-48px>
189
+ <a-divider>
190
+ <a-button
191
+ class="h-48px!"
192
+ type="link"
193
+ block
194
+ @click="auth.signIn('4A')"
195
+ >
196
+ 前往4A登录
197
+ </a-button>
198
+ </a-divider>
199
+ </div>
200
+ </a-form-item>
201
+ </AForm>
202
+ </div>
203
+ </template>
204
+
205
+ <style scoped lang="scss">
206
+ .login-form {
207
+ margin-top: 40px;
208
+
209
+ :deep(.ant-input-group-addon) {
210
+ padding: 0 !important;
211
+ overflow: hidden;
212
+ }
213
+ }
214
+ </style>
@@ -0,0 +1,17 @@
1
+ <script setup lang="ts">
2
+ defineOptions({
3
+ name: 'Savephone',
4
+ });
5
+
6
+ const checked = defineModel(
7
+ 'checked',
8
+ {
9
+ type: Boolean, default: false,
10
+ });
11
+ </script>
12
+
13
+ <template>
14
+ <a-checkbox v-model:checked="checked">
15
+ 记住手机号
16
+ </a-checkbox>
17
+ </template>
@@ -0,0 +1,26 @@
1
+ <script setup lang="ts">
2
+ defineOptions({
3
+ name: 'Useragreement',
4
+ });
5
+
6
+ const checked = defineModel(
7
+ 'checked',
8
+ {
9
+ type: Boolean, default: true,
10
+ });
11
+
12
+ function jumpUseragreementPage() {
13
+ // 暂未实现用户协议、隐私政策
14
+ }
15
+ </script>
16
+
17
+ <template>
18
+ <a-checkbox v-model:checked="checked">
19
+ 我已阅读并同意<span
20
+ hover:underline
21
+ decoration-2
22
+ text="#1578FF"
23
+ @click.prevent="jumpUseragreementPage"
24
+ >用户协议、隐私政策</span>
25
+ </a-checkbox>
26
+ </template>
@@ -0,0 +1,84 @@
1
+ import type { Component, MaybeRef } from 'vue';
2
+ import { getAuthLoginValidCode } from '@/api/modules/auth';
3
+ import { useActiveElement } from '@vueuse/core';
4
+ import { useRequest } from 'alova/client';
5
+
6
+ interface LoginTabs {
7
+ title: string
8
+ component: Component
9
+ }
10
+
11
+ export function useLoginTabs() {
12
+ const tabs: LoginTabs[] = [
13
+ {
14
+ title: '账号登录',
15
+ component: defineAsyncComponent(() => import('./components/PasswordLogin.vue')),
16
+ },
17
+ {
18
+ title: '手机号登录',
19
+ component: defineAsyncComponent(() => import('./components/LoginWithPhone.vue')),
20
+ },
21
+ ];
22
+ const initialTab = ref(0);
23
+
24
+ function changeActiveTab(activeTab: number): void {
25
+ initialTab.value = activeTab;
26
+ }
27
+ return {
28
+ tabs,
29
+ initialTab,
30
+ changeActiveTab,
31
+ };
32
+ }
33
+
34
+ export function useCaptchas() {
35
+ const { send, onSuccess } = useRequest(getAuthLoginValidCode({ size: '100x40' }));
36
+ const captcha = ref('');
37
+ const captchaHash = ref('');
38
+
39
+ onSuccess(({ data }) => {
40
+ const { key, plaintext } = data.data!;
41
+
42
+ const imgUrl = `data:image/png;base64,${plaintext}`;
43
+ captcha.value = imgUrl;
44
+ captchaHash.value = key!;
45
+ });
46
+ return {
47
+ send,
48
+ captcha,
49
+ captchaHash,
50
+ };
51
+ }
52
+
53
+ export function useFocusElement() {
54
+ const activeElement = useActiveElement();
55
+ const focusElementDataId = computed(() =>
56
+ activeElement.value?.dataset?.id || 'null',
57
+ );
58
+ return {
59
+ focusElementDataId,
60
+ };
61
+ }
62
+
63
+ export function useCountdown(initialTime: MaybeRef<number> = 60) {
64
+ const count = ref(toValue(initialTime));
65
+ const isStart = computed(() => {
66
+ return toValue(count) !== toValue(initialTime);
67
+ });
68
+
69
+ function start() {
70
+ count.value--;
71
+ const timer = setInterval(() => {
72
+ count.value--;
73
+ if (count.value === 0) {
74
+ clearInterval(timer);
75
+ count.value = toValue(initialTime);
76
+ }
77
+ }, 1000);
78
+ }
79
+ return {
80
+ count,
81
+ start,
82
+ isStart,
83
+ };
84
+ }