verce-vue-test 0.0.26 → 0.0.28
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/mini2.0-main/env.d.ts +1 -0
- package/mini2.0-main/package-lock.json +21 -0
- package/mini2.0-main/package.json +2 -0
- package/mini2.0-main/plugins/bump-version.ts +19 -8
- package/mini2.0-main/src/App.vue +15 -2
- package/mini2.0-main/src/config/plugin.properties.pro +2 -2
- package/mini2.0-main/src/config/plugin.properties.test +2 -2
- package/mini2.0-main/src/core/mxApi/index.ts +28 -1
- package/mini2.0-main/src/main.ts +1 -5
- package/mini2.0-main/src/router/index.ts +2 -2
- package/mini2.0-main/src/stores/app.ts +29 -0
- package/mini2.0-main/src/stores/user.ts +1 -1
- package/mini2.0-main/src/views/home/index.vue +8 -8
- package/mini2.0-main/src/views/login/index.vue +6 -5
- package/mini2.0-main/vite.config.ts +1 -0
- package/package.json +1 -1
- package/src/App.vue +4 -0
- package/src/components/ElButtonPro.vue +215 -0
- package/src/components/ElUploadButton.vue +261 -0
- package/src/components/NumberRange.vue +262 -0
- package/src/router/index.ts +6 -0
- package/src/views/FundCockpitView.vue +1064 -0
- package/src/views/PlaceholderView.vue +387 -15
package/mini2.0-main/env.d.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@iconify/vue": "^5.0.1",
|
|
12
12
|
"@vueuse/core": "^14.3.0",
|
|
13
|
+
"adm-zip": "^0.5.17",
|
|
13
14
|
"axios": "^1.16.1",
|
|
14
15
|
"clipboard": "^2.0.11",
|
|
15
16
|
"crypto-js": "^4.2.0",
|
|
@@ -25,6 +26,7 @@
|
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
27
28
|
"@tsconfig/node24": "^24.0.4",
|
|
29
|
+
"@types/adm-zip": "^0.5.8",
|
|
28
30
|
"@types/node": "^24.12.2",
|
|
29
31
|
"@vitejs/plugin-vue": "^6.0.6",
|
|
30
32
|
"@vue/eslint-config-typescript": "^14.7.0",
|
|
@@ -1463,6 +1465,16 @@
|
|
|
1463
1465
|
"tslib": "^2.4.0"
|
|
1464
1466
|
}
|
|
1465
1467
|
},
|
|
1468
|
+
"node_modules/@types/adm-zip": {
|
|
1469
|
+
"version": "0.5.8",
|
|
1470
|
+
"resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.8.tgz",
|
|
1471
|
+
"integrity": "sha512-RVVH7QvZYbN+ihqZ4kX/dMiowf6o+Jk1fNwiSdx0NahBJLU787zkULhGhJM8mf/obmLGmgdMM0bXsQTmyfbR7Q==",
|
|
1472
|
+
"dev": true,
|
|
1473
|
+
"license": "MIT",
|
|
1474
|
+
"dependencies": {
|
|
1475
|
+
"@types/node": "*"
|
|
1476
|
+
}
|
|
1477
|
+
},
|
|
1466
1478
|
"node_modules/@types/esrecurse": {
|
|
1467
1479
|
"version": "4.3.1",
|
|
1468
1480
|
"resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
|
|
@@ -2198,6 +2210,15 @@
|
|
|
2198
2210
|
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
|
2199
2211
|
}
|
|
2200
2212
|
},
|
|
2213
|
+
"node_modules/adm-zip": {
|
|
2214
|
+
"version": "0.5.17",
|
|
2215
|
+
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz",
|
|
2216
|
+
"integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==",
|
|
2217
|
+
"license": "MIT",
|
|
2218
|
+
"engines": {
|
|
2219
|
+
"node": ">=12.0"
|
|
2220
|
+
}
|
|
2221
|
+
},
|
|
2201
2222
|
"node_modules/agent-base": {
|
|
2202
2223
|
"version": "6.0.2",
|
|
2203
2224
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@iconify/vue": "^5.0.1",
|
|
24
24
|
"@vueuse/core": "^14.3.0",
|
|
25
|
+
"adm-zip": "^0.5.17",
|
|
25
26
|
"axios": "^1.16.1",
|
|
26
27
|
"clipboard": "^2.0.11",
|
|
27
28
|
"crypto-js": "^4.2.0",
|
|
@@ -37,6 +38,7 @@
|
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
40
|
"@tsconfig/node24": "^24.0.4",
|
|
41
|
+
"@types/adm-zip": "^0.5.8",
|
|
40
42
|
"@types/node": "^24.12.2",
|
|
41
43
|
"@vitejs/plugin-vue": "^6.0.6",
|
|
42
44
|
"@vue/eslint-config-typescript": "^14.7.0",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, cpSync } from 'node:fs'
|
|
1
|
+
import { readFileSync, writeFileSync, cpSync, rmSync } from 'node:fs'
|
|
2
2
|
import { resolve, basename } from 'node:path'
|
|
3
|
-
import { execSync } from 'node:child_process'
|
|
4
3
|
import type { Plugin } from 'vite'
|
|
4
|
+
import AdmZip from 'adm-zip'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* 打包时自增版本号、复制配置文件、压缩 dist 为 zip
|
|
@@ -23,10 +23,14 @@ export function bumpVersion(mode: string): Plugin {
|
|
|
23
23
|
const oldCode = Number(match[1])
|
|
24
24
|
const newCode = oldCode + 1
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
26
|
+
// 版本号规则:5位 version_code → 4位版本名
|
|
27
|
+
// version_code: ABCDD A=major B=minor C=patch DD=hotfix
|
|
28
|
+
// version_name: A.B.C.DD 示例 66102 → 6.6.1.02
|
|
29
|
+
const major = Math.floor(newCode / 10000)
|
|
30
|
+
const minor = Math.floor((newCode / 1000) % 10)
|
|
31
|
+
const patch = Math.floor((newCode / 100) % 10)
|
|
32
|
+
const hotfix = String(newCode % 100).padStart(2, '0')
|
|
33
|
+
const versionName = `${major}.${minor}.${patch}.${hotfix}`
|
|
30
34
|
|
|
31
35
|
content = content
|
|
32
36
|
.replace(/version_code=\d+/, `version_code=${newCode}`)
|
|
@@ -40,10 +44,17 @@ export function bumpVersion(mode: string): Plugin {
|
|
|
40
44
|
|
|
41
45
|
console.log(`[plugin.properties] version: ${oldCode} → ${newCode} (${versionName})`)
|
|
42
46
|
|
|
43
|
-
// 压缩 dist 为 zip
|
|
47
|
+
// 压缩 dist 为 zip(使用 adm-zip,跨平台兼容)
|
|
44
48
|
const distDir = resolve(outDir, '..')
|
|
45
49
|
const zipName = `dist_${newCode}.zip`
|
|
46
|
-
|
|
50
|
+
const zipPath = resolve(distDir, zipName)
|
|
51
|
+
const zip = new AdmZip()
|
|
52
|
+
|
|
53
|
+
zip.addLocalFolder(resolve(distDir, 'www'), 'www')
|
|
54
|
+
zip.addLocalFile(resolve(distDir, 'plugin.properties'))
|
|
55
|
+
|
|
56
|
+
rmSync(zipPath, { force: true })
|
|
57
|
+
zip.writeZip(zipPath)
|
|
47
58
|
console.log(`[zip] 已生成 ${basename(distDir)}/${zipName}`)
|
|
48
59
|
},
|
|
49
60
|
}
|
package/mini2.0-main/src/App.vue
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
|
-
<script setup lang="ts"
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useAppStore } from '@/stores/app'
|
|
3
|
+
|
|
4
|
+
const appStore = useAppStore()
|
|
5
|
+
|
|
6
|
+
onMounted(async () => {
|
|
7
|
+
await appStore.initNativeDetection()
|
|
8
|
+
|
|
9
|
+
// 测试环境 + 原生设备:加载 vConsole 调试面板
|
|
10
|
+
if (import.meta.env.VITE_APP_ENV === 'test' && appStore.isNative) {
|
|
11
|
+
import('vconsole').then(({ default: VConsole }) => new VConsole())
|
|
12
|
+
}
|
|
13
|
+
})
|
|
14
|
+
</script>
|
|
2
15
|
|
|
3
16
|
<template>
|
|
4
|
-
<router-view v-slot="{ Component }">
|
|
17
|
+
<router-view v-if="appStore.ready" v-slot="{ Component }">
|
|
5
18
|
<keep-alive include="Home">
|
|
6
19
|
<component :is="Component" />
|
|
7
20
|
</keep-alive>
|
|
@@ -232,6 +232,33 @@ export const ajaxPut = <T = unknown>(url: string, data?: unknown): Promise<MXAja
|
|
|
232
232
|
export const ajaxDelete = <T = unknown>(url: string, id: string): Promise<MXAjaxResponse<T>> =>
|
|
233
233
|
ajax<T>({ type: 'DELETE', url: `${url}/${id}` })
|
|
234
234
|
|
|
235
|
-
|
|
235
|
+
let nativeReady = false
|
|
236
|
+
let resolveNativeReady: (() => void) | null = null
|
|
237
|
+
|
|
238
|
+
const nativeReadyPromise = new Promise<void>((resolve) => {
|
|
239
|
+
resolveNativeReady = resolve
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
document.addEventListener('deviceready', () => {
|
|
243
|
+
nativeReady = true
|
|
244
|
+
resolveNativeReady?.()
|
|
245
|
+
}, false)
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 等待原生设备就绪(deviceready 事件触发后 resolve)
|
|
249
|
+
*
|
|
250
|
+
* native 环境下 deviceready 触发后 window.MXCommon 才注入完成,
|
|
251
|
+
* 之后 isNativeApp() 才能拿到正确结果。
|
|
252
|
+
*/
|
|
253
|
+
export const whenNativeReady = (): Promise<boolean> => {
|
|
254
|
+
if (nativeReady) return Promise.resolve(isNativeApp())
|
|
255
|
+
|
|
256
|
+
return Promise.race([
|
|
257
|
+
nativeReadyPromise.then(() => isNativeApp()),
|
|
258
|
+
new Promise<boolean>((resolve) => setTimeout(() => resolve(false), 3000)),
|
|
259
|
+
])
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** 检测是否在原生环境(需在 deviceready 之后调用才准确) */
|
|
236
263
|
export const isNativeApp = (): boolean =>
|
|
237
264
|
typeof window.MXCommon !== 'undefined'
|
package/mini2.0-main/src/main.ts
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import { createApp } from 'vue'
|
|
2
2
|
import { createPinia } from 'pinia'
|
|
3
3
|
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
|
4
|
-
import 'vant/lib/index.css'
|
|
4
|
+
import 'vant/lib/index.css'
|
|
5
5
|
|
|
6
6
|
import App from './App.vue'
|
|
7
7
|
import router from './router'
|
|
8
8
|
|
|
9
|
-
if (import.meta.env.VITE_APP_ENV !== 'production') {
|
|
10
|
-
import('vconsole').then(({ default: VConsole }) => new VConsole())
|
|
11
|
-
}
|
|
12
|
-
|
|
13
9
|
const app = createApp(App)
|
|
14
10
|
const pinia = createPinia()
|
|
15
11
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { createRouter,
|
|
1
|
+
import { createRouter, createWebHashHistory } from 'vue-router'
|
|
2
2
|
import { useUserStore } from '@/stores/user'
|
|
3
3
|
|
|
4
4
|
const router = createRouter({
|
|
5
|
-
history:
|
|
5
|
+
history: createWebHashHistory(),
|
|
6
6
|
routes: [
|
|
7
7
|
{
|
|
8
8
|
path: '/',
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { whenNativeReady } from '@/core/mxApi'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 应用级状态 Store
|
|
6
|
+
*
|
|
7
|
+
* 存放全局共享的运行时状态,不持久化。
|
|
8
|
+
*/
|
|
9
|
+
export const useAppStore = defineStore('app', () => {
|
|
10
|
+
/** 原生环境检测是否已完成(App.vue onMounted 后变为 true) */
|
|
11
|
+
const ready = ref(false)
|
|
12
|
+
|
|
13
|
+
/** 是否在原生 APP WebView 环境中(由 App.vue 初始化时检测并写入) */
|
|
14
|
+
const isNative = ref(false)
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 初始化原生环境检测
|
|
18
|
+
*
|
|
19
|
+
* 等待 deviceready 事件后判断 window.MXCommon 是否存在,
|
|
20
|
+
* 浏览器环境下 3 秒超时后返回 false。
|
|
21
|
+
* 应在 App.vue onMounted 中调用,全局只需一次。
|
|
22
|
+
*/
|
|
23
|
+
async function initNativeDetection() {
|
|
24
|
+
isNative.value = await whenNativeReady()
|
|
25
|
+
ready.value = true
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return { ready, isNative, initNativeDetection }
|
|
29
|
+
})
|
|
@@ -13,7 +13,7 @@ export const useUserStore = defineStore(
|
|
|
13
13
|
// -------------------------------- State --------------------------------
|
|
14
14
|
|
|
15
15
|
/** 登录凭证,登录成功后由后端返回,后续请求通过请求头携带 */
|
|
16
|
-
const token = ref('')
|
|
16
|
+
const token = ref('7')
|
|
17
17
|
|
|
18
18
|
// ------------------------------ Actions --------------------------------
|
|
19
19
|
|
|
@@ -7,44 +7,44 @@ type HomeFeature = {
|
|
|
7
7
|
|
|
8
8
|
defineOptions({ name: 'HomePage' })
|
|
9
9
|
|
|
10
|
-
const bannerImage =
|
|
10
|
+
const bannerImage = `${import.meta.env.BASE_URL}images/img.png`
|
|
11
11
|
|
|
12
12
|
const router = useRouter()
|
|
13
13
|
|
|
14
14
|
const features: HomeFeature[] = [
|
|
15
15
|
{
|
|
16
16
|
title: ['人民币头寸', '预报及查询'],
|
|
17
|
-
image:
|
|
17
|
+
image: `${import.meta.env.BASE_URL}images/f5876785-b927-4347-ba19-999114240649.png`,
|
|
18
18
|
route: '/rmb-position',
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
title: ['外币头寸', '预报及查询'],
|
|
22
|
-
image:
|
|
22
|
+
image: `${import.meta.env.BASE_URL}images/12a73787-86a9-4891-a65f-66104746f6a8.png`,
|
|
23
23
|
route: '/foreign-position',
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
title: ['人行头寸', '余额查询'],
|
|
27
|
-
image:
|
|
27
|
+
image: `${import.meta.env.BASE_URL}images/73fef1e4-0fd0-4a1a-9b8b-a70a5b6acbbc.png`,
|
|
28
28
|
route: '/pbc-position',
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
title: ['头寸匡算'],
|
|
32
|
-
image:
|
|
32
|
+
image: `${import.meta.env.BASE_URL}images/ea745a10-42aa-4f44-8d7f-3ab02cc0adcd.png`,
|
|
33
33
|
route: '/position-estimate',
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
title: ['人民币净借记', '可用额度管理'],
|
|
37
|
-
image:
|
|
37
|
+
image: `${import.meta.env.BASE_URL}images/5798d7aa-ba8b-4605-8079-58b35495ac55.png`,
|
|
38
38
|
route: '/net-debit',
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
41
|
title: ['人民币小额网银', '清算场次明细'],
|
|
42
|
-
image:
|
|
42
|
+
image: `${import.meta.env.BASE_URL}images/c3dbbd9d-be56-490e-b9f4-6ee17ebefffc.png`,
|
|
43
43
|
route: '/clearing-detail',
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
46
|
title: ['预警信息'],
|
|
47
|
-
image:
|
|
47
|
+
image: `${import.meta.env.BASE_URL}images/bc685b4c-0cca-4a79-924c-a8ee10e6f8eb.png`,
|
|
48
48
|
route: '/warning',
|
|
49
49
|
},
|
|
50
50
|
]
|
|
@@ -3,11 +3,12 @@ import { useRouter } from 'vue-router'
|
|
|
3
3
|
import { login } from '@/api/user'
|
|
4
4
|
import { showToast } from 'vant'
|
|
5
5
|
import { useUserStore } from '@/stores/user'
|
|
6
|
-
import {
|
|
6
|
+
import { useAppStore } from '@/stores/app'
|
|
7
|
+
import { getEncryptString } from '@/core/mxApi'
|
|
7
8
|
|
|
8
9
|
const router = useRouter()
|
|
9
10
|
const userStore = useUserStore()
|
|
10
|
-
const
|
|
11
|
+
const appStore = useAppStore()
|
|
11
12
|
|
|
12
13
|
const form = ref({
|
|
13
14
|
username: '',
|
|
@@ -18,12 +19,12 @@ const loading = ref(false)
|
|
|
18
19
|
|
|
19
20
|
// 原生环境:从客户端获取密钥,直接交换 token
|
|
20
21
|
onMounted(async () => {
|
|
21
|
-
if (!
|
|
22
|
+
if (!appStore.isNative) return
|
|
22
23
|
|
|
23
24
|
loading.value = true
|
|
24
25
|
try {
|
|
25
26
|
const secret = await getEncryptString()
|
|
26
|
-
|
|
27
|
+
console.log(secret)
|
|
27
28
|
} catch {
|
|
28
29
|
// 错误已由 request.ts 拦截器统一处理
|
|
29
30
|
} finally {
|
|
@@ -49,7 +50,7 @@ async function onSubmit() {
|
|
|
49
50
|
<template>
|
|
50
51
|
<div class="login-page">
|
|
51
52
|
<!-- 原生环境:自动登录,只显示加载状态 -->
|
|
52
|
-
<van-loading v-if="isNative" vertical class="native-loading">
|
|
53
|
+
<van-loading v-if="appStore.isNative" vertical class="native-loading">
|
|
53
54
|
正在登录...
|
|
54
55
|
</van-loading>
|
|
55
56
|
|
package/package.json
CHANGED
package/src/App.vue
CHANGED
|
@@ -89,6 +89,10 @@ function toggleCollapse() {
|
|
|
89
89
|
<el-icon><TrendCharts /></el-icon>
|
|
90
90
|
<span>总行管理员首页</span>
|
|
91
91
|
</el-menu-item>
|
|
92
|
+
<el-menu-item index="/fund-cockpit">
|
|
93
|
+
<el-icon><TrendCharts /></el-icon>
|
|
94
|
+
<span>资金驾驶舱</span>
|
|
95
|
+
</el-menu-item>
|
|
92
96
|
<el-menu-item index="/notice-announcement">
|
|
93
97
|
<el-icon><Bell /></el-icon>
|
|
94
98
|
<span>通知公告</span>
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed, useAttrs } from 'vue'
|
|
3
|
+
import { ElMessageBox, ElMessage } from 'element-plus'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 增强版按钮组件
|
|
7
|
+
* 基于 el-button,提供权限控制、防抖、确认对话框等功能
|
|
8
|
+
*/
|
|
9
|
+
defineOptions({
|
|
10
|
+
name: 'ElButtonPro',
|
|
11
|
+
inheritAttrs: false,
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const attrs = useAttrs()
|
|
15
|
+
|
|
16
|
+
const props = withDefaults(
|
|
17
|
+
defineProps<{
|
|
18
|
+
/** 权限码,用于权限控制 */
|
|
19
|
+
permission?: string | string[]
|
|
20
|
+
/** 是否启用防抖,单位毫秒 */
|
|
21
|
+
debounce?: number
|
|
22
|
+
/** 是否启用节流,单位毫秒 */
|
|
23
|
+
throttle?: number
|
|
24
|
+
/** 点击前是否显示确认对话框 */
|
|
25
|
+
confirm?: boolean | string
|
|
26
|
+
/** 确认对话框标题 */
|
|
27
|
+
confirmTitle?: string
|
|
28
|
+
/** 确认对话框类型 */
|
|
29
|
+
confirmType?: 'warning' | 'info' | 'success' | 'error'
|
|
30
|
+
/** 是否显示取消按钮 */
|
|
31
|
+
showCancelButton?: boolean
|
|
32
|
+
/** 取消按钮文本 */
|
|
33
|
+
cancelButtonText?: string
|
|
34
|
+
/** 确认按钮文本 */
|
|
35
|
+
confirmButtonText?: string
|
|
36
|
+
/** 确认按钮类型 */
|
|
37
|
+
confirmButtonType?: 'primary' | 'success' | 'warning' | 'danger' | 'info'
|
|
38
|
+
/** 是否显示成功提示 */
|
|
39
|
+
showSuccessMessage?: boolean | string
|
|
40
|
+
/** 成功提示文本 */
|
|
41
|
+
successMessage?: string
|
|
42
|
+
/** 是否显示失败提示 */
|
|
43
|
+
showErrorMessage?: boolean
|
|
44
|
+
/** 失败提示文本 */
|
|
45
|
+
errorMessage?: string
|
|
46
|
+
/** 是否自动处理错误 */
|
|
47
|
+
autoHandleError?: boolean
|
|
48
|
+
}>(),
|
|
49
|
+
{
|
|
50
|
+
debounce: 0,
|
|
51
|
+
throttle: 0,
|
|
52
|
+
confirm: false,
|
|
53
|
+
confirmTitle: '提示',
|
|
54
|
+
confirmType: 'warning',
|
|
55
|
+
showCancelButton: true,
|
|
56
|
+
cancelButtonText: '取消',
|
|
57
|
+
confirmButtonText: '确定',
|
|
58
|
+
confirmButtonType: 'primary',
|
|
59
|
+
showSuccessMessage: false,
|
|
60
|
+
successMessage: '操作成功',
|
|
61
|
+
showErrorMessage: true,
|
|
62
|
+
errorMessage: '操作失败',
|
|
63
|
+
autoHandleError: true,
|
|
64
|
+
},
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
const emit = defineEmits<{
|
|
68
|
+
/** 点击事件 */
|
|
69
|
+
'click': [event: MouseEvent]
|
|
70
|
+
/** 确认后点击事件 */
|
|
71
|
+
'confirm': [event: MouseEvent]
|
|
72
|
+
/** 成功事件 */
|
|
73
|
+
'success': [data?: any]
|
|
74
|
+
/** 失败事件 */
|
|
75
|
+
'error': [error: Error]
|
|
76
|
+
}>()
|
|
77
|
+
|
|
78
|
+
/** 内部加载状态 */
|
|
79
|
+
const internalLoading = ref(false)
|
|
80
|
+
|
|
81
|
+
/** 从 attrs 中提取 disabled */
|
|
82
|
+
const disabled = computed(() => attrs.disabled as boolean | undefined)
|
|
83
|
+
|
|
84
|
+
/** 从 attrs 中提取外部 loading */
|
|
85
|
+
const externalLoading = computed(() => attrs.loading as boolean | undefined)
|
|
86
|
+
|
|
87
|
+
/** 最终的加载状态(合并外部 loading 和内部 loading) */
|
|
88
|
+
const finalLoading = computed(() => externalLoading.value || internalLoading.value)
|
|
89
|
+
|
|
90
|
+
/** 过滤后的 attrs(移除 loading 和 disabled) */
|
|
91
|
+
const filteredAttrs = computed(() => {
|
|
92
|
+
const { loading, disabled: _, ...rest } = attrs
|
|
93
|
+
return rest
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
/** 防抖/节流定时器 */
|
|
97
|
+
let timer: ReturnType<typeof setTimeout> | null = null
|
|
98
|
+
let lastTime = 0
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 处理点击事件
|
|
102
|
+
*/
|
|
103
|
+
const handleClick = async (event: MouseEvent) => {
|
|
104
|
+
// 如果按钮被禁用或正在加载,不处理点击
|
|
105
|
+
if (disabled.value || finalLoading.value) {
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 防抖处理
|
|
110
|
+
if (props.debounce > 0) {
|
|
111
|
+
if (timer) {
|
|
112
|
+
clearTimeout(timer)
|
|
113
|
+
}
|
|
114
|
+
timer = setTimeout(() => {
|
|
115
|
+
handleConfirmClick(event)
|
|
116
|
+
}, props.debounce)
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 节流处理
|
|
121
|
+
if (props.throttle > 0) {
|
|
122
|
+
const now = Date.now()
|
|
123
|
+
if (now - lastTime < props.throttle) {
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
lastTime = now
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
await handleConfirmClick(event)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 处理确认后的点击事件
|
|
134
|
+
*/
|
|
135
|
+
const handleConfirmClick = async (event: MouseEvent) => {
|
|
136
|
+
// 如果需要确认对话框
|
|
137
|
+
if (props.confirm) {
|
|
138
|
+
try {
|
|
139
|
+
const confirmText = typeof props.confirm === 'string' ? props.confirm : '确定要执行此操作吗?'
|
|
140
|
+
await ElMessageBox.confirm(confirmText, props.confirmTitle, {
|
|
141
|
+
confirmButtonText: props.confirmButtonText,
|
|
142
|
+
cancelButtonText: props.cancelButtonText,
|
|
143
|
+
type: props.confirmType,
|
|
144
|
+
showCancelButton: props.showCancelButton,
|
|
145
|
+
confirmButtonClass: `el-button--${props.confirmButtonType}`,
|
|
146
|
+
})
|
|
147
|
+
} catch {
|
|
148
|
+
// 用户取消操作
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 触发点击事件
|
|
154
|
+
emit('click', event)
|
|
155
|
+
emit('confirm', event)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 执行异步操作
|
|
160
|
+
*/
|
|
161
|
+
const execute = async <T = any>(fn: () => Promise<T>): Promise<T | undefined> => {
|
|
162
|
+
if (finalLoading.value) {
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
internalLoading.value = true
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const result = await fn()
|
|
170
|
+
|
|
171
|
+
// 显示成功提示
|
|
172
|
+
if (props.showSuccessMessage) {
|
|
173
|
+
const message = typeof props.showSuccessMessage === 'string' ? props.showSuccessMessage : props.successMessage
|
|
174
|
+
ElMessage.success(message)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
emit('success', result)
|
|
178
|
+
return result
|
|
179
|
+
} catch (error) {
|
|
180
|
+
const err = error instanceof Error ? error : new Error(String(error))
|
|
181
|
+
|
|
182
|
+
// 显示失败提示
|
|
183
|
+
if (props.showErrorMessage) {
|
|
184
|
+
const message = props.errorMessage || err.message
|
|
185
|
+
ElMessage.error(message)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
emit('error', err)
|
|
189
|
+
|
|
190
|
+
if (!props.autoHandleError) {
|
|
191
|
+
throw err
|
|
192
|
+
}
|
|
193
|
+
} finally {
|
|
194
|
+
internalLoading.value = false
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 暴露方法给父组件
|
|
200
|
+
*/
|
|
201
|
+
defineExpose({
|
|
202
|
+
execute,
|
|
203
|
+
loading: finalLoading,
|
|
204
|
+
})
|
|
205
|
+
</script>
|
|
206
|
+
|
|
207
|
+
<template>
|
|
208
|
+
<el-button
|
|
209
|
+
:loading="finalLoading"
|
|
210
|
+
v-bind="filteredAttrs"
|
|
211
|
+
@click="handleClick"
|
|
212
|
+
>
|
|
213
|
+
<slot />
|
|
214
|
+
</el-button>
|
|
215
|
+
</template>
|