fix: 优化watch 修改编辑用户弹窗样式

This commit is contained in:
ZhuRui
2026-07-27 09:51:30 +08:00
parent 17d5c85904
commit e3e66ec03b
11 changed files with 146 additions and 161 deletions
+5 -8
View File
@@ -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 { Modal, Form, Input, Button, message } from 'ant-design-vue';
import { TIPS_TEXT, newPasswordRules, confirmNewPasswordRules } from './controller'; import { TIPS_TEXT, newPasswordRules, confirmNewPasswordRules } from './controller';
import styles from './style.module.less'; import styles from './style.module.less';
@@ -30,16 +31,12 @@ export default defineComponent({
const formData = reactive(getDefaultForm()); const formData = reactive(getDefaultForm());
/** 监听 visible 重置表单 */ /** 监听 visible 重置表单 */
watch( useEffect(() => {
() => props.visible, if (props.visible) {
(val) => {
if (val) {
Object.assign(formData, getDefaultForm()); Object.assign(formData, getDefaultForm());
setTimeout(() => formRef.value?.clearValidate(), 0); setTimeout(() => formRef.value?.clearValidate(), 0);
} }
}, }, [() => props.visible]);
{ immediate: true },
);
/** 提交 */ /** 提交 */
const handleSubmit = async () => { const handleSubmit = async () => {
+4 -3
View File
@@ -1,6 +1,6 @@
// src/hooks/useAuth.ts // src/hooks/useAuth.ts
import { useState } from './useState'; import { useState } from './useState';
import { watch } from 'vue'; import { useEffect } from './useEffect';
import { useMenuStore } from '@/stores/menuStore'; import { useMenuStore } from '@/stores/menuStore';
import { usePermissionStore } from '@/stores/permissionStore'; import { usePermissionStore } from '@/stores/permissionStore';
import router from '@/router'; import router from '@/router';
@@ -10,13 +10,14 @@ const TOKEN_KEY = 'MY_APP_AUTH_TOKEN';
function createAuth() { function createAuth() {
const [token, setToken] = useState<string | null>(window.localStorage.getItem(TOKEN_KEY)); const [token, setToken] = useState<string | null>(window.localStorage.getItem(TOKEN_KEY));
watch(token, (newToken) => { useEffect(() => {
const newToken = token.value;
if (newToken) { if (newToken) {
window.localStorage.setItem(TOKEN_KEY, newToken); window.localStorage.setItem(TOKEN_KEY, newToken);
} else { } else {
window.localStorage.removeItem(TOKEN_KEY); window.localStorage.removeItem(TOKEN_KEY);
} }
}); }, [token]);
const isLoggedIn = () => !!token.value; const isLoggedIn = () => !!token.value;
+6 -3
View File
@@ -1,6 +1,7 @@
// src/hooks/useDebounce.ts // src/hooks/useDebounce.ts
import { watch, onUnmounted, Ref } from 'vue'; import { onUnmounted, Ref } from 'vue';
import { useState } from './useState'; import { useState } from './useState';
import { useEffect } from './useEffect';
export interface UseDebounceOptions { export interface UseDebounceOptions {
delay?: number; delay?: number;
@@ -40,7 +41,7 @@ export function useDebounce<T>(source: Ref<T>, options: number | UseDebounceOpti
isFirstCall = true; isFirstCall = true;
}; };
watch(source, () => { useEffect(() => {
setIsPending(true); setIsPending(true);
// 处理 maxWait // 处理 maxWait
@@ -61,7 +62,9 @@ export function useDebounce<T>(source: Ref<T>, options: number | UseDebounceOpti
updateValue(); updateValue();
}, delay); }, delay);
} }
});
return clearTimers;
}, [source]);
// 卸载 // 卸载
onUnmounted(() => { onUnmounted(() => {
+13 -16
View File
@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/ban-ts-comment */
// src/hooks/usePagination.ts // 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 pageSizeRef = ref(options.pageSize ?? defaultPageSize);
const totalRef = ref(options.total ?? 0); const totalRef = ref(options.total ?? 0);
watch( useEffect(() => {
() => options.current, const val = options.current;
(val) => {
if (val !== undefined) currentRef.value = val; if (val !== undefined) currentRef.value = val;
}, }, [() => options.current]);
);
watch( useEffect(() => {
() => options.pageSize, const val = options.pageSize;
(val) => {
if (val !== undefined) pageSizeRef.value = val; if (val !== undefined) pageSizeRef.value = val;
}, }, [() => options.pageSize]);
);
watch( useEffect(() => {
() => options.total, const val = options.total;
(val) => {
if (val !== undefined) totalRef.value = val; if (val !== undefined) totalRef.value = val;
}, }, [() => options.total]);
);
const totalPages = computed(() => { const totalPages = computed(() => {
const total = totalRef.value; const total = totalRef.value;
+12 -13
View File
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */ /* 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<T> { export interface UseRequestReturn<T> {
/** 纯净的数据,不包含后端外层壳 */ /** 纯净的数据,不包含后端外层壳 */
@@ -128,9 +129,8 @@ export function useRequest<T>(
const shouldAutoRun = computed(() => !manual && ready.value); const shouldAutoRun = computed(() => !manual && ready.value);
let hasAutoRun = false; // 标记是否已自动执行过,防止重复触发 let hasAutoRun = false; // 标记是否已自动执行过,防止重复触发
watch( useEffect(() => {
shouldAutoRun, const val = shouldAutoRun.value;
(val) => {
if (val && !hasAutoRun) { if (val && !hasAutoRun) {
hasAutoRun = true; hasAutoRun = true;
run(); run();
@@ -138,21 +138,20 @@ export function useRequest<T>(
// 当条件不满足时重置 hasAutoRun,使下次满足条件时能再次自动执行 // 当条件不满足时重置 hasAutoRun,使下次满足条件时能再次自动执行
hasAutoRun = false; hasAutoRun = false;
} }
}, }, [shouldAutoRun]);
{ immediate: true },
);
// refreshDeps 变化时重新请求(初始化时不再重复触发) // refreshDeps 变化时重新请求(初始化时不再重复触发)
if (refreshDeps && refreshDeps.length > 0) { if (refreshDeps && refreshDeps.length > 0) {
watch( let skipInitialRefresh = true;
refreshDeps, useEffect(() => {
() => { if (skipInitialRefresh) {
skipInitialRefresh = false;
return;
}
if (shouldAutoRun.value) { if (shouldAutoRun.value) {
run(); run();
} }
}, }, refreshDeps);
{ deep: true },
);
} }
const getSignal = () => abortController?.signal; const getSignal = () => abortController?.signal;
+9 -6
View File
@@ -1,5 +1,6 @@
import { ref, watch, onBeforeUnmount, toValue } from 'vue'; import { ref, onBeforeUnmount, toValue } from 'vue';
import type { Ref, MaybeRefOrGetter } from 'vue'; import type { Ref, MaybeRefOrGetter } from 'vue';
import { useEffect } from './useEffect';
// --- 类型定义 --- // --- 类型定义 ---
export type WebSocketStatus = 'connecting' | 'open' | 'closed' | 'error'; export type WebSocketStatus = 'connecting' | 'open' | 'closed' | 'error';
@@ -216,14 +217,16 @@ export function useWebSocket(
// --- 监听与副作用 --- // --- 监听与副作用 ---
// 监听 URL 变化,如果变了则重新连接 // 监听 URL 变化,如果变了则重新连接
watch( let skipInitialUrlWatch = true;
() => toValue(url), useEffect(() => {
() => { if (skipInitialUrlWatch) {
skipInitialUrlWatch = false;
return;
}
if (status.value !== 'closed') { if (status.value !== 'closed') {
connect(); connect();
} }
}, }, [() => toValue(url)]);
);
// 组件卸载时清理资源 // 组件卸载时清理资源
onBeforeUnmount(() => { onBeforeUnmount(() => {
@@ -1,4 +1,5 @@
import { defineComponent, ref, reactive, watch } from 'vue'; import { defineComponent, ref, reactive } from 'vue';
import { useEffect } from '@/hooks';
import { import {
Modal, Modal,
Form, Form,
@@ -119,23 +120,18 @@ export default defineComponent({
}; };
/** 监听 visible 变化重置表单 */ /** 监听 visible 变化重置表单 */
watch( useEffect(() => {
() => props.visible, if (props.visible) {
(val) => {
if (val) {
initFormFromRecord(props.record); initFormFromRecord(props.record);
cropperVisible.value = false; cropperVisible.value = false;
uploadImageUrl.value = ''; uploadImageUrl.value = '';
setTimeout(() => formRef.value?.clearValidate(), 0); setTimeout(() => formRef.value?.clearValidate(), 0);
} }
}, }, [() => props.visible]);
{ immediate: true },
);
/** 监听 jumpType 变化时清理联动字段 */ /** 监听 jumpType 变化时清理联动字段 */
watch( useEffect(() => {
() => formData.jumpType, const newType = formData.jumpType;
(newType) => {
if (newType !== 'event_detail') formData.relatedEventId = ''; if (newType !== 'event_detail') formData.relatedEventId = '';
if (newType !== 'miniprogram_page') formData.pagePath = ''; if (newType !== 'miniprogram_page') formData.pagePath = '';
if (newType !== 'custom_link') formData.customUrl = ''; if (newType !== 'custom_link') formData.customUrl = '';
@@ -144,8 +140,7 @@ export default defineComponent({
if (newType === 'miniprogram_page') fieldsToValidate.push('pagePath'); if (newType === 'miniprogram_page') fieldsToValidate.push('pagePath');
if (newType === 'custom_link') fieldsToValidate.push('customUrl'); if (newType === 'custom_link') fieldsToValidate.push('customUrl');
if (fieldsToValidate.length) formRef.value?.clearValidate(fieldsToValidate); if (fieldsToValidate.length) formRef.value?.clearValidate(fieldsToValidate);
}, }, [() => formData.jumpType]);
);
/** 触发隐藏的文件选择器 */ /** 触发隐藏的文件选择器 */
const triggerFilePicker = () => { const triggerFilePicker = () => {
@@ -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 { Modal, Form, Input, Tree, Button } from 'ant-design-vue';
import type { TreeProps } from 'ant-design-vue'; import type { TreeProps } from 'ant-design-vue';
import { roleNameRules, PERMISSION_TREE } from '../controller'; import { roleNameRules, PERMISSION_TREE } from '../controller';
@@ -60,16 +61,12 @@ export default defineComponent({
}; };
/** 监听 visible 重置 */ /** 监听 visible 重置 */
watch( useEffect(() => {
() => props.visible, if (props.visible) {
(val) => {
if (val) {
initFormFromRecord(props.record); initFormFromRecord(props.record);
setTimeout(() => formRef.value?.clearValidate(), 0); setTimeout(() => formRef.value?.clearValidate(), 0);
} }
}, }, [() => props.visible]);
{ immediate: true },
);
/** Tree 勾选回调(受控) */ /** Tree 勾选回调(受控) */
const handleCheck: TreeProps['onCheck'] = (checked) => { const handleCheck: TreeProps['onCheck'] = (checked) => {
@@ -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 { 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'; import styles from './UserListModal.module.less';
interface UserListModalProps { interface UserListModalProps {
@@ -35,12 +35,9 @@ export default defineComponent({
}); });
/** 监听 visible/users 变化重置分页 */ /** 监听 visible/users 变化重置分页 */
watch( useEffect(() => {
() => [props.visible, props.users.length],
() => {
setPagination({ current: 1, pageSize: 10, total: filteredUsers.value.length }); setPagination({ current: 1, pageSize: 10, total: filteredUsers.value.length });
}, }, [() => props.visible, () => props.users.length]);
);
const handleSearch = () => { const handleSearch = () => {
setPagination({ ...pagination.value, total: filteredUsers.value.length, current: 1 }); setPagination({ ...pagination.value, total: filteredUsers.value.length, current: 1 });
@@ -20,25 +20,16 @@
} }
} }
/* ===== 编辑模式密码区 ===== */ /* ===== 密码区重置链接 ===== */
.passwordEditRow { .resetLink {
display: flex; font-size: 14px;
align-items: center; color: #1677ff;
gap: 8px; cursor: pointer;
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);
user-select: none; user-select: none;
}
.resetBtn { &:hover {
flex-shrink: 0; color: #4096ff;
}
} }
/* ===== 取消重置行 ===== */ /* ===== 取消重置行 ===== */
@@ -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 { Modal, Form, Input, Select, Button } from 'ant-design-vue';
import { import {
ROLE_FORM_OPTIONS, ROLE_FORM_OPTIONS,
@@ -62,17 +63,13 @@ export default defineComponent({
}; };
/** 监听 visible 变化重置表单 */ /** 监听 visible 变化重置表单 */
watch( useEffect(() => {
() => props.visible, if (props.visible) {
(val) => {
if (val) {
initFormFromRecord(props.record); initFormFromRecord(props.record);
isResetting.value = false; isResetting.value = false;
setTimeout(() => formRef.value?.clearValidate(), 0); setTimeout(() => formRef.value?.clearValidate(), 0);
} }
}, }, [() => props.visible]);
{ immediate: true },
);
/** 点击"重置密码" */ /** 点击"重置密码" */
const handleResetPassword = () => { const handleResetPassword = () => {
@@ -146,19 +143,28 @@ export default defineComponent({
</Form.Item> </Form.Item>
</div> </div>
{/* ====== 密码区 ====== */} {/* ====== 密码区 + 角色 ====== */}
{isEdit && !isResetting.value ? ( {isEdit && !isResetting.value ? (
/* 编辑模式 - 密码占位 + 重置密码按钮 */ /* 编辑模式未重置:密码占位 + 角色 同一行 */
<div class={styles.row}> <div class={styles.row}>
<Form.Item label="密码" class={styles.col}> <Form.Item label="密码" class={styles.col}>
<div class={styles.passwordEditRow}> <Input
<span class={styles.passwordMask}>{PASSWORD_PLACEHOLDER}</span> value={PASSWORD_PLACEHOLDER}
<Button size="small" class={styles.resetBtn} onClick={handleResetPassword}> readonly
suffix={
<a class={styles.resetLink} onClick={handleResetPassword}>
</Button> </a>
</div> }
/>
</Form.Item>
<Form.Item label="角色" name="role" rules={roleRules} class={styles.col}>
<Select
options={ROLE_FORM_OPTIONS as any}
placeholder="请选择角色"
v-model:value={formData.role}
/>
</Form.Item> </Form.Item>
<div class={styles.col} />
</div> </div>
) : ( ) : (
<> <>
@@ -189,10 +195,7 @@ export default defineComponent({
/> />
</Form.Item> </Form.Item>
</div> </div>
</> {/* 角色:单独一行 */}
)}
{/* 角色:单独一行,固定宽度 */}
<Form.Item label="角色" name="role" rules={roleRules}> <Form.Item label="角色" name="role" rules={roleRules}>
<Select <Select
options={ROLE_FORM_OPTIONS as any} options={ROLE_FORM_OPTIONS as any}
@@ -201,6 +204,8 @@ export default defineComponent({
style={{ width: '200px' }} style={{ width: '200px' }}
/> />
</Form.Item> </Form.Item>
</>
)}
</Form> </Form>
{/* 底部按钮 */} {/* 底部按钮 */}