From e3e66ec03bcf3945d3780108c086427fa6d22eb0 Mon Sep 17 00:00:00 2001 From: ZhuRui Date: Mon, 27 Jul 2026 09:51:30 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96watch=20=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E7=BC=96=E8=BE=91=E7=94=A8=E6=88=B7=E5=BC=B9=E7=AA=97?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ChangePasswordModal/index.tsx | 19 +++--- src/hooks/useAuth.ts | 7 +- src/hooks/useDebounce.ts | 9 ++- src/hooks/usePagination.ts | 35 +++++----- src/hooks/useRequest.ts | 45 ++++++------- src/hooks/useWebSocket.ts | 21 +++--- .../banner/components/BannerFormModal.tsx | 47 ++++++------- .../system/roles/components/RoleFormModal.tsx | 19 +++--- .../system/roles/components/UserListModal.tsx | 13 ++-- .../components/UserFormModal.module.less | 25 +++---- .../system/users/components/UserFormModal.tsx | 67 ++++++++++--------- 11 files changed, 146 insertions(+), 161 deletions(-) diff --git a/src/components/ChangePasswordModal/index.tsx b/src/components/ChangePasswordModal/index.tsx index e103b6c..4c61711 100644 --- a/src/components/ChangePasswordModal/index.tsx +++ b/src/components/ChangePasswordModal/index.tsx @@ -1,4 +1,5 @@ -import { defineComponent, ref, reactive, watch } from 'vue'; +import { defineComponent, ref, reactive } from 'vue'; +import { useEffect } from '@/hooks'; import { Modal, Form, Input, Button, message } from 'ant-design-vue'; import { TIPS_TEXT, newPasswordRules, confirmNewPasswordRules } from './controller'; import styles from './style.module.less'; @@ -30,16 +31,12 @@ export default defineComponent({ const formData = reactive(getDefaultForm()); /** 监听 visible 重置表单 */ - watch( - () => props.visible, - (val) => { - if (val) { - Object.assign(formData, getDefaultForm()); - setTimeout(() => formRef.value?.clearValidate(), 0); - } - }, - { immediate: true }, - ); + useEffect(() => { + if (props.visible) { + Object.assign(formData, getDefaultForm()); + setTimeout(() => formRef.value?.clearValidate(), 0); + } + }, [() => props.visible]); /** 提交 */ const handleSubmit = async () => { diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index 5eca581..275228a 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -1,6 +1,6 @@ // src/hooks/useAuth.ts import { useState } from './useState'; -import { watch } from 'vue'; +import { useEffect } from './useEffect'; import { useMenuStore } from '@/stores/menuStore'; import { usePermissionStore } from '@/stores/permissionStore'; import router from '@/router'; @@ -10,13 +10,14 @@ const TOKEN_KEY = 'MY_APP_AUTH_TOKEN'; function createAuth() { const [token, setToken] = useState(window.localStorage.getItem(TOKEN_KEY)); - watch(token, (newToken) => { + useEffect(() => { + const newToken = token.value; if (newToken) { window.localStorage.setItem(TOKEN_KEY, newToken); } else { window.localStorage.removeItem(TOKEN_KEY); } - }); + }, [token]); const isLoggedIn = () => !!token.value; diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts index 65f99ce..30393d1 100644 --- a/src/hooks/useDebounce.ts +++ b/src/hooks/useDebounce.ts @@ -1,6 +1,7 @@ // src/hooks/useDebounce.ts -import { watch, onUnmounted, Ref } from 'vue'; +import { onUnmounted, Ref } from 'vue'; import { useState } from './useState'; +import { useEffect } from './useEffect'; export interface UseDebounceOptions { delay?: number; @@ -40,7 +41,7 @@ export function useDebounce(source: Ref, options: number | UseDebounceOpti isFirstCall = true; }; - watch(source, () => { + useEffect(() => { setIsPending(true); // 处理 maxWait @@ -61,7 +62,9 @@ export function useDebounce(source: Ref, options: number | UseDebounceOpti updateValue(); }, delay); } - }); + + return clearTimers; + }, [source]); // 卸载 onUnmounted(() => { diff --git a/src/hooks/usePagination.ts b/src/hooks/usePagination.ts index eeafc3c..d3a0a13 100644 --- a/src/hooks/usePagination.ts +++ b/src/hooks/usePagination.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ // src/hooks/usePagination.ts -import { computed, ref, watch, Ref, triggerRef } from 'vue'; +import { computed, ref, Ref, triggerRef } from 'vue'; +import { useEffect } from './useEffect'; /** * 分页配置项 @@ -77,24 +78,20 @@ export function usePagination(options: UsePaginationOptions = {}): UsePagination const pageSizeRef = ref(options.pageSize ?? defaultPageSize); const totalRef = ref(options.total ?? 0); - watch( - () => options.current, - (val) => { - if (val !== undefined) currentRef.value = val; - }, - ); - watch( - () => options.pageSize, - (val) => { - if (val !== undefined) pageSizeRef.value = val; - }, - ); - watch( - () => options.total, - (val) => { - if (val !== undefined) totalRef.value = val; - }, - ); + useEffect(() => { + const val = options.current; + if (val !== undefined) currentRef.value = val; + }, [() => options.current]); + + useEffect(() => { + const val = options.pageSize; + if (val !== undefined) pageSizeRef.value = val; + }, [() => options.pageSize]); + + useEffect(() => { + const val = options.total; + if (val !== undefined) totalRef.value = val; + }, [() => options.total]); const totalPages = computed(() => { const total = totalRef.value; diff --git a/src/hooks/useRequest.ts b/src/hooks/useRequest.ts index bd1076c..ce984e7 100644 --- a/src/hooks/useRequest.ts +++ b/src/hooks/useRequest.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import { ref, Ref, watch, WatchSource, onUnmounted, computed } from 'vue'; +import { ref, Ref, WatchSource, onUnmounted, computed } from 'vue'; +import { useEffect } from './useEffect'; export interface UseRequestReturn { /** 纯净的数据,不包含后端外层壳 */ @@ -128,31 +129,29 @@ export function useRequest( const shouldAutoRun = computed(() => !manual && ready.value); let hasAutoRun = false; // 标记是否已自动执行过,防止重复触发 - watch( - shouldAutoRun, - (val) => { - if (val && !hasAutoRun) { - hasAutoRun = true; - run(); - } else if (!val) { - // 当条件不满足时重置 hasAutoRun,使下次满足条件时能再次自动执行 - hasAutoRun = false; - } - }, - { immediate: true }, - ); + useEffect(() => { + const val = shouldAutoRun.value; + if (val && !hasAutoRun) { + hasAutoRun = true; + run(); + } else if (!val) { + // 当条件不满足时重置 hasAutoRun,使下次满足条件时能再次自动执行 + hasAutoRun = false; + } + }, [shouldAutoRun]); // refreshDeps 变化时重新请求(初始化时不再重复触发) if (refreshDeps && refreshDeps.length > 0) { - watch( - refreshDeps, - () => { - if (shouldAutoRun.value) { - run(); - } - }, - { deep: true }, - ); + let skipInitialRefresh = true; + useEffect(() => { + if (skipInitialRefresh) { + skipInitialRefresh = false; + return; + } + if (shouldAutoRun.value) { + run(); + } + }, refreshDeps); } const getSignal = () => abortController?.signal; diff --git a/src/hooks/useWebSocket.ts b/src/hooks/useWebSocket.ts index 2952105..4cde23b 100644 --- a/src/hooks/useWebSocket.ts +++ b/src/hooks/useWebSocket.ts @@ -1,5 +1,6 @@ -import { ref, watch, onBeforeUnmount, toValue } from 'vue'; +import { ref, onBeforeUnmount, toValue } from 'vue'; import type { Ref, MaybeRefOrGetter } from 'vue'; +import { useEffect } from './useEffect'; // --- 类型定义 --- export type WebSocketStatus = 'connecting' | 'open' | 'closed' | 'error'; @@ -216,14 +217,16 @@ export function useWebSocket( // --- 监听与副作用 --- // 监听 URL 变化,如果变了则重新连接 - watch( - () => toValue(url), - () => { - if (status.value !== 'closed') { - connect(); - } - }, - ); + let skipInitialUrlWatch = true; + useEffect(() => { + if (skipInitialUrlWatch) { + skipInitialUrlWatch = false; + return; + } + if (status.value !== 'closed') { + connect(); + } + }, [() => toValue(url)]); // 组件卸载时清理资源 onBeforeUnmount(() => { diff --git a/src/pages/events/banner/components/BannerFormModal.tsx b/src/pages/events/banner/components/BannerFormModal.tsx index 2327240..e7ac703 100644 --- a/src/pages/events/banner/components/BannerFormModal.tsx +++ b/src/pages/events/banner/components/BannerFormModal.tsx @@ -1,4 +1,5 @@ -import { defineComponent, ref, reactive, watch } from 'vue'; +import { defineComponent, ref, reactive } from 'vue'; +import { useEffect } from '@/hooks'; import { Modal, Form, @@ -119,33 +120,27 @@ export default defineComponent({ }; /** 监听 visible 变化重置表单 */ - watch( - () => props.visible, - (val) => { - if (val) { - initFormFromRecord(props.record); - cropperVisible.value = false; - uploadImageUrl.value = ''; - setTimeout(() => formRef.value?.clearValidate(), 0); - } - }, - { immediate: true }, - ); + useEffect(() => { + if (props.visible) { + initFormFromRecord(props.record); + cropperVisible.value = false; + uploadImageUrl.value = ''; + setTimeout(() => formRef.value?.clearValidate(), 0); + } + }, [() => props.visible]); /** 监听 jumpType 变化时清理联动字段 */ - watch( - () => formData.jumpType, - (newType) => { - if (newType !== 'event_detail') formData.relatedEventId = ''; - if (newType !== 'miniprogram_page') formData.pagePath = ''; - if (newType !== 'custom_link') formData.customUrl = ''; - const fieldsToValidate: string[] = []; - if (newType === 'event_detail') fieldsToValidate.push('relatedEventId'); - if (newType === 'miniprogram_page') fieldsToValidate.push('pagePath'); - if (newType === 'custom_link') fieldsToValidate.push('customUrl'); - if (fieldsToValidate.length) formRef.value?.clearValidate(fieldsToValidate); - }, - ); + useEffect(() => { + const newType = formData.jumpType; + if (newType !== 'event_detail') formData.relatedEventId = ''; + if (newType !== 'miniprogram_page') formData.pagePath = ''; + if (newType !== 'custom_link') formData.customUrl = ''; + const fieldsToValidate: string[] = []; + if (newType === 'event_detail') fieldsToValidate.push('relatedEventId'); + if (newType === 'miniprogram_page') fieldsToValidate.push('pagePath'); + if (newType === 'custom_link') fieldsToValidate.push('customUrl'); + if (fieldsToValidate.length) formRef.value?.clearValidate(fieldsToValidate); + }, [() => formData.jumpType]); /** 触发隐藏的文件选择器 */ const triggerFilePicker = () => { diff --git a/src/pages/system/roles/components/RoleFormModal.tsx b/src/pages/system/roles/components/RoleFormModal.tsx index 3df6fc5..86296d7 100644 --- a/src/pages/system/roles/components/RoleFormModal.tsx +++ b/src/pages/system/roles/components/RoleFormModal.tsx @@ -1,4 +1,5 @@ -import { defineComponent, ref, reactive, watch } from 'vue'; +import { defineComponent, ref, reactive } from 'vue'; +import { useEffect } from '@/hooks'; import { Modal, Form, Input, Tree, Button } from 'ant-design-vue'; import type { TreeProps } from 'ant-design-vue'; import { roleNameRules, PERMISSION_TREE } from '../controller'; @@ -60,16 +61,12 @@ export default defineComponent({ }; /** 监听 visible 重置 */ - watch( - () => props.visible, - (val) => { - if (val) { - initFormFromRecord(props.record); - setTimeout(() => formRef.value?.clearValidate(), 0); - } - }, - { immediate: true }, - ); + useEffect(() => { + if (props.visible) { + initFormFromRecord(props.record); + setTimeout(() => formRef.value?.clearValidate(), 0); + } + }, [() => props.visible]); /** Tree 勾选回调(受控) */ const handleCheck: TreeProps['onCheck'] = (checked) => { diff --git a/src/pages/system/roles/components/UserListModal.tsx b/src/pages/system/roles/components/UserListModal.tsx index 08a6796..7f5af7e 100644 --- a/src/pages/system/roles/components/UserListModal.tsx +++ b/src/pages/system/roles/components/UserListModal.tsx @@ -1,6 +1,6 @@ -import { defineComponent, ref, reactive, computed, watch } from 'vue'; +import { defineComponent, ref, reactive, computed } from 'vue'; import { Modal, Form, Input, Table, Button, Space, Pagination } from 'ant-design-vue'; -import { useState } from '@/hooks'; +import { useEffect, useState } from '@/hooks'; import styles from './UserListModal.module.less'; interface UserListModalProps { @@ -35,12 +35,9 @@ export default defineComponent({ }); /** 监听 visible/users 变化重置分页 */ - watch( - () => [props.visible, props.users.length], - () => { - setPagination({ current: 1, pageSize: 10, total: filteredUsers.value.length }); - }, - ); + useEffect(() => { + setPagination({ current: 1, pageSize: 10, total: filteredUsers.value.length }); + }, [() => props.visible, () => props.users.length]); const handleSearch = () => { setPagination({ ...pagination.value, total: filteredUsers.value.length, current: 1 }); diff --git a/src/pages/system/users/components/UserFormModal.module.less b/src/pages/system/users/components/UserFormModal.module.less index d31433e..3791736 100644 --- a/src/pages/system/users/components/UserFormModal.module.less +++ b/src/pages/system/users/components/UserFormModal.module.less @@ -20,25 +20,16 @@ } } -/* ===== 编辑模式密码区 ===== */ -.passwordEditRow { - display: flex; - align-items: center; - gap: 8px; - height: 32px; - line-height: 32px; -} - -.passwordMask { - font-family: 'Courier New', Courier, monospace; - font-size: 16px; - letter-spacing: 1px; - color: rgba(0, 0, 0, 0.65); +/* ===== 密码区重置链接 ===== */ +.resetLink { + font-size: 14px; + color: #1677ff; + cursor: pointer; user-select: none; -} -.resetBtn { - flex-shrink: 0; + &:hover { + color: #4096ff; + } } /* ===== 取消重置行 ===== */ diff --git a/src/pages/system/users/components/UserFormModal.tsx b/src/pages/system/users/components/UserFormModal.tsx index f75e600..5ca74fa 100644 --- a/src/pages/system/users/components/UserFormModal.tsx +++ b/src/pages/system/users/components/UserFormModal.tsx @@ -1,4 +1,5 @@ -import { defineComponent, ref, reactive, watch } from 'vue'; +import { defineComponent, ref, reactive } from 'vue'; +import { useEffect } from '@/hooks'; import { Modal, Form, Input, Select, Button } from 'ant-design-vue'; import { ROLE_FORM_OPTIONS, @@ -62,17 +63,13 @@ export default defineComponent({ }; /** 监听 visible 变化重置表单 */ - watch( - () => props.visible, - (val) => { - if (val) { - initFormFromRecord(props.record); - isResetting.value = false; - setTimeout(() => formRef.value?.clearValidate(), 0); - } - }, - { immediate: true }, - ); + useEffect(() => { + if (props.visible) { + initFormFromRecord(props.record); + isResetting.value = false; + setTimeout(() => formRef.value?.clearValidate(), 0); + } + }, [() => props.visible]); /** 点击"重置密码" */ const handleResetPassword = () => { @@ -146,19 +143,28 @@ export default defineComponent({ - {/* ====== 密码区 ====== */} + {/* ====== 密码区 + 角色 ====== */} {isEdit && !isResetting.value ? ( - /* 编辑模式 - 密码占位 + 重置密码按钮 */ + /* 编辑模式未重置:密码占位 + 角色 同一行 */
-
- {PASSWORD_PLACEHOLDER} - -
+ + 重置密码 + + } + /> +
+ + + )} - - {/* 角色:单独一行,固定宽度 */} - -