feat: 新增扫码登录功能并优化 SSE、请求拦截与多页面适配
This commit is contained in:
1 parent
484e7047f5
commit
907b07f552
30 files changed
+259
-142
No files matched your search
+2
-2
@@ -292,8 +292,8 @@ export interface UserForm {
|
||||
mobile?: string;
|
||||
/** 昵称 */
|
||||
nickname?: string;
|
||||
/** 角色ID集合 */
|
||||
roleIds: number[];
|
||||
/** 角色ID集合(后端 Long 序列化为字符串,兼容数字) */
|
||||
roleIds: (number | string)[];
|
||||
/** 用户状态(1:正常;0:禁用) */
|
||||
status?: number;
|
||||
/** 用户名 */
|
||||
|
||||
@@ -63,7 +63,7 @@ const handleConfirm = () => {
|
||||
const startDate = dayjs(dateRange.value[0]).format("YYYY-MM-DD");
|
||||
const endDate = dayjs(dateRange.value[1]).format("YYYY-MM-DD");
|
||||
|
||||
let newVal: any = [startDate, endDate];
|
||||
let newVal: string | string[] = [startDate, endDate];
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
newVal = `${startDate},${endDate}`;
|
||||
|
||||
@@ -30,8 +30,9 @@
|
||||
</view>
|
||||
|
||||
<view class="custom-navbar__right" :style="{ width: rightWidth + 'px' }">
|
||||
<view class="custom-navbar__capsule-space" :style="{ width: capsuleSpaceWidth + 'px' }" />
|
||||
<slot name="right" />
|
||||
<!-- 微信端胶囊按钮占位,保证插槽内容不会顶到胶囊下方 -->
|
||||
<view class="custom-navbar__capsule-space" :style="{ width: capsuleSpaceWidth + 'px' }" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
import { ref, computed, watch } from "vue";
|
||||
|
||||
export interface TreeOption {
|
||||
[key: string]: any;
|
||||
[key: string]: unknown;
|
||||
value: string;
|
||||
label: string;
|
||||
children?: TreeOption[];
|
||||
@@ -163,7 +163,7 @@ const flatNodes = computed(() => {
|
||||
result.push({
|
||||
...node,
|
||||
value: String(node[props.valueKey]),
|
||||
label: node[props.labelKey],
|
||||
label: node[props.labelKey] as string,
|
||||
level,
|
||||
hasChildren: !!(children && children.length > 0),
|
||||
raw: node,
|
||||
|
||||
@@ -2,7 +2,6 @@ import { checkLogin } from "@/utils/auth";
|
||||
|
||||
interface NavItem {
|
||||
url?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export function useNavigation() {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { checkLogin } from "@/utils/auth";
|
||||
|
||||
/**
|
||||
* 扫码登录 PC 端
|
||||
*
|
||||
* 调起相机扫描 PC 端登录二维码,校验票据后跳转扫码确认页。
|
||||
* 未登录时跳转登录页,用户取消扫码静默处理。
|
||||
*/
|
||||
export function useScanLogin() {
|
||||
const scanLogin = async () => {
|
||||
if (!checkLogin()) return;
|
||||
try {
|
||||
const res = await uni.scanCode({ scanType: ["qrCode"] });
|
||||
const ticket = (res.result || "").trim();
|
||||
if (!ticket) {
|
||||
uni.showToast({ title: "无效的二维码", icon: "none" });
|
||||
return;
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: `/pages/mine/scan-confirm/index?ticket=${encodeURIComponent(ticket)}`,
|
||||
});
|
||||
} catch {
|
||||
// 用户取消扫码,静默处理
|
||||
}
|
||||
};
|
||||
|
||||
return { scanLogin };
|
||||
}
|
||||
@@ -9,7 +9,7 @@ export interface UseSseOptions {
|
||||
maxReconnectAttempts?: number;
|
||||
}
|
||||
|
||||
type EventHandler = (data: any) => void;
|
||||
type EventHandler = (data: unknown) => void;
|
||||
|
||||
export enum SseConnectionState {
|
||||
DISCONNECTED = "DISCONNECTED",
|
||||
@@ -44,8 +44,8 @@ function createSseConnection(options: UseSseOptions = {}) {
|
||||
|
||||
const eventHandlers = new Map<string, Set<EventHandler>>();
|
||||
|
||||
const log = (...args: any[]) => config.debug && console.log("[SSE]", ...args);
|
||||
const logError = (...args: any[]) => console.error("[SSE]", ...args);
|
||||
const log = (...args: unknown[]) => config.debug && console.log("[SSE]", ...args);
|
||||
const logError = (...args: unknown[]) => console.error("[SSE]", ...args);
|
||||
|
||||
const clearTimer = (timer: typeof connectionTimeoutTimer) => {
|
||||
if (timer) {
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<template>
|
||||
<view :class="`wot-theme-${theme}`" :style="themeCSSVars">
|
||||
<wd-config-provider :theme-vars="themeVars" :theme="theme === 'dark' ? 'dark' : ''" :button="{ type: 'primary' }" :tag="{ variant: 'plain' }">
|
||||
<wd-config-provider
|
||||
:theme-vars="themeVars"
|
||||
:theme="theme === 'dark' ? 'dark' : ''"
|
||||
:button="{ type: 'primary' }"
|
||||
:tag="{ variant: 'plain' }"
|
||||
>
|
||||
<slot />
|
||||
<wd-tabbar
|
||||
v-model="active"
|
||||
|
||||
+21
-19
@@ -120,9 +120,7 @@
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<view class="login__form-item">
|
||||
<wd-button block :loading="isLoading" @click="handleLogin">
|
||||
登 录
|
||||
</wd-button>
|
||||
<wd-button block :loading="isLoading" @click="handleLogin">登 录</wd-button>
|
||||
</view>
|
||||
|
||||
<!-- 切换登录方式 -->
|
||||
@@ -243,9 +241,7 @@
|
||||
<text class="login__demo-hint-text">演示环境验证码:123456</text>
|
||||
</view>
|
||||
|
||||
<wd-button block :loading="isBindLoading" @click="handleBindMobile">
|
||||
确认绑定
|
||||
</wd-button>
|
||||
<wd-button block :loading="isBindLoading" @click="handleBindMobile">确认绑定</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
@@ -264,6 +260,7 @@ import type { FormSchema } from "@wot-ui/ui/components/wd-form/types";
|
||||
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
import { useCountdown } from "@/composables/useCountdown";
|
||||
import { getErrorMessage } from "@/utils";
|
||||
import AuthAPI from "@/api/auth";
|
||||
|
||||
definePage({
|
||||
@@ -432,8 +429,8 @@ async function doFormLogin() {
|
||||
await userStore.getInfo();
|
||||
toast.success("登录成功");
|
||||
setTimeout(() => uni.reLaunch({ url: redirect.value }), 800);
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || "登录失败");
|
||||
} catch (error) {
|
||||
toast.error(getErrorMessage(error, "登录失败"));
|
||||
if (loginMode.value === "PASSWORD") fetchCaptcha();
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
@@ -475,13 +472,18 @@ const handleSendCode = async () => {
|
||||
await AuthAPI.sendSmsLoginCode(mobile);
|
||||
toast.success("验证码已发送");
|
||||
startSmsCountdown();
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || "发送失败");
|
||||
} catch (error) {
|
||||
toast.error(getErrorMessage(error, "发送失败"));
|
||||
}
|
||||
};
|
||||
|
||||
// 微信登录
|
||||
const handleWechatPhoneLogin = async (e: any) => {
|
||||
/** 微信手机号快捷填充按钮回调事件 */
|
||||
interface WxPhoneLoginEvent {
|
||||
detail: { code?: string };
|
||||
}
|
||||
|
||||
const handleWechatPhoneLogin = async (e: WxPhoneLoginEvent) => {
|
||||
const phoneCode = e.detail.code;
|
||||
if (!isAgreePolicy.value) {
|
||||
openPolicyDialog("WECHAT_PHONE", phoneCode);
|
||||
@@ -518,7 +520,7 @@ const handleWechatSilentLogin = async () => {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const { code } = await uni.login();
|
||||
const result: any = await userStore.loginByWxMa(code);
|
||||
const result = await userStore.loginByWxMa(code);
|
||||
if (result.needBindMobile && result.openid) {
|
||||
wechatOpenid.value = result.openid;
|
||||
showBindMobilePopup.value = true;
|
||||
@@ -527,8 +529,8 @@ const handleWechatSilentLogin = async () => {
|
||||
toast.success("登录成功");
|
||||
setTimeout(() => uni.reLaunch({ url: redirect.value }), 800);
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || "微信登录失败");
|
||||
} catch (error) {
|
||||
toast.error(getErrorMessage(error, "微信登录失败"));
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
@@ -546,8 +548,8 @@ const handleSendBindCode = async () => {
|
||||
await AuthAPI.sendSmsLoginCode(mobile);
|
||||
toast.success("验证码已发送");
|
||||
startBindSmsCountdown();
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || "发送失败");
|
||||
} catch (error) {
|
||||
toast.error(getErrorMessage(error, "发送失败"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -575,8 +577,8 @@ const handleBindMobile = async () => {
|
||||
resetBindForm();
|
||||
toast.success("绑定成功");
|
||||
setTimeout(() => uni.reLaunch({ url: redirect.value }), 800);
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || "绑定失败");
|
||||
} catch (error) {
|
||||
toast.error(getErrorMessage(error, "绑定失败"));
|
||||
} finally {
|
||||
isBindLoading.value = false;
|
||||
}
|
||||
@@ -597,7 +599,7 @@ const handleBack = () => {
|
||||
};
|
||||
|
||||
// 生命周期
|
||||
onLoad((options: any) => {
|
||||
onLoad((options) => {
|
||||
const fromQuery = options?.redirect ? decodeURIComponent(options.redirect) : "";
|
||||
if (fromQuery && fromQuery !== "/pages/login/index") redirect.value = fromQuery;
|
||||
uni.setNavigationBarTitle({ title: "" });
|
||||
|
||||
+23
-31
@@ -11,6 +11,11 @@
|
||||
:show-back="false"
|
||||
:placeholder="false"
|
||||
>
|
||||
<template #left>
|
||||
<view v-if="isLogin" class="mine-navbar__scan" aria-label="扫一扫" @click="scanLogin">
|
||||
<wd-icon name="scan" size="40rpx" color="var(--color-text-inverse)" />
|
||||
</view>
|
||||
</template>
|
||||
<template #center>
|
||||
<text class="mine-navbar__title">个人中心</text>
|
||||
</template>
|
||||
@@ -65,9 +70,7 @@
|
||||
</view>
|
||||
|
||||
<view v-if="!isLogin" class="profile-card__action" @click.stop="navigateToLogin()">
|
||||
<wd-button custom-class="profile-card__button" size="small">
|
||||
立即登录
|
||||
</wd-button>
|
||||
<wd-button custom-class="profile-card__button" size="small">立即登录</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -132,16 +135,6 @@
|
||||
</view>
|
||||
<wd-icon name="right" size="16" color="var(--color-text-placeholder)" />
|
||||
</view>
|
||||
<view v-if="isLogin" class="menu-row" @click="handleScan">
|
||||
<view class="menu-row__icon menu-row__icon--info">
|
||||
<wd-icon name="scan" size="18" color="var(--color-info)" />
|
||||
</view>
|
||||
<view class="menu-row__main">
|
||||
<text class="menu-row__title">扫一扫</text>
|
||||
<text class="menu-row__desc">扫描二维码登录 PC</text>
|
||||
</view>
|
||||
<wd-icon name="right" size="16" color="var(--color-text-placeholder)" />
|
||||
</view>
|
||||
<view class="menu-row" @click="handleClearCache">
|
||||
<view class="menu-row__icon menu-row__icon--danger">
|
||||
<wd-icon name="delete" size="18" color="var(--color-danger)" />
|
||||
@@ -211,6 +204,7 @@ import { onShow } from "@dcloudio/uni-app";
|
||||
import { useUserStore, useThemeStore } from "@/store";
|
||||
import { useRouter } from "uni-mini-router";
|
||||
import { useNavbar } from "@/composables/useNavbar";
|
||||
import { useScanLogin } from "@/composables/useScanLogin";
|
||||
import { getAccessToken } from "@/utils/auth";
|
||||
import { formatBytes } from "@/utils/format";
|
||||
|
||||
@@ -388,24 +382,8 @@ const handleClearCache = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 扫一扫:扫描 PC 端二维码后跳转确认页
|
||||
const handleScan = async () => {
|
||||
if (!isLogin.value) {
|
||||
navigateToLogin();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await uni.scanCode({ scanType: ["qrCode"] });
|
||||
const ticket = (res.result || "").trim();
|
||||
if (!ticket) {
|
||||
uni.showToast({ title: "无效的二维码", icon: "none" });
|
||||
return;
|
||||
}
|
||||
router.push({ path: "/pages/mine/scan-confirm/index", query: { ticket: encodeURIComponent(ticket) } });
|
||||
} catch {
|
||||
// 用户取消扫码,静默处理
|
||||
}
|
||||
};
|
||||
// 扫一扫:扫描 PC 端二维码后跳转确认页(逻辑收敛在 useScanLogin)
|
||||
const { scanLogin } = useScanLogin();
|
||||
|
||||
// 有来技术公众号
|
||||
const openOfficialAccount = () => {
|
||||
@@ -455,6 +433,20 @@ const openOfficialAccount = () => {
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
/* 扫码入口:渐变背景上的白色图标,64rpx 圆形热区 */
|
||||
.mine-navbar__scan {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
|
||||
&:active {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.profile-tag {
|
||||
display: inline-flex;
|
||||
gap: 8rpx;
|
||||
|
||||
@@ -133,6 +133,7 @@ import { ref, reactive, computed } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { useDialog, useToast } from "@wot-ui/ui";
|
||||
import { useUserStore } from "@/store";
|
||||
import { getErrorMessage } from "@/utils";
|
||||
import UserAPI, { type UserProfileForm } from "@/api/user";
|
||||
import FileAPI, { type FileInfo } from "@/api/file";
|
||||
|
||||
@@ -165,7 +166,7 @@ const canComplete = computed(() => {
|
||||
return profileForm.nickname && profileForm.mobile;
|
||||
});
|
||||
|
||||
onLoad((options: any) => {
|
||||
onLoad((options) => {
|
||||
if (options?.redirect) {
|
||||
redirect.value = decodeURIComponent(options.redirect);
|
||||
}
|
||||
@@ -173,11 +174,21 @@ onLoad((options: any) => {
|
||||
if (userInfo) {
|
||||
profileForm.nickname = userInfo.nickname || "";
|
||||
profileForm.avatar = userInfo.avatar || "";
|
||||
profileForm.gender = (userInfo as any).gender || 1;
|
||||
profileForm.gender = userInfo.gender || 1;
|
||||
}
|
||||
});
|
||||
|
||||
const onChooseAvatar = async (e: any) => {
|
||||
/** 微信头像选择按钮回调事件(open-type="chooseAvatar") */
|
||||
interface WxChooseAvatarEvent {
|
||||
detail: { avatarUrl: string };
|
||||
}
|
||||
|
||||
/** wd-img-cropper 裁剪确认事件 */
|
||||
interface ImgCropperConfirmEvent {
|
||||
tempFilePath: string;
|
||||
}
|
||||
|
||||
const onChooseAvatar = async (e: WxChooseAvatarEvent) => {
|
||||
try {
|
||||
const { avatarUrl } = e.detail;
|
||||
toast.loading("上传中...");
|
||||
@@ -220,7 +231,7 @@ const chooseAvatar = () => {
|
||||
// #endif
|
||||
};
|
||||
|
||||
const handleAvatarConfirm = async (event: any) => {
|
||||
const handleAvatarConfirm = async (event: ImgCropperConfirmEvent) => {
|
||||
try {
|
||||
const { tempFilePath } = event;
|
||||
toast.loading("上传中...");
|
||||
@@ -260,8 +271,8 @@ const handleComplete = async () => {
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: redirect.value });
|
||||
}, 1000);
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || "完善信息失败");
|
||||
} catch (error) {
|
||||
toast.error(getErrorMessage(error, "完善信息失败"));
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
@@ -99,7 +99,12 @@ function handleAvatarUpload() {
|
||||
});
|
||||
}
|
||||
// 头像裁剪完成
|
||||
function handleAvatarConfirm(event: any) {
|
||||
/** wd-img-cropper 裁剪确认事件 */
|
||||
interface ImgCropperConfirmEvent {
|
||||
tempFilePath: string;
|
||||
}
|
||||
|
||||
function handleAvatarConfirm(event: ImgCropperConfirmEvent) {
|
||||
const { tempFilePath } = event;
|
||||
FileAPI.upload(tempFilePath).then((fileInfo: FileInfo) => {
|
||||
const avatarForm: UserProfileForm = {
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { useToast } from "@wot-ui/ui";
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
import { getErrorMessage } from "@/utils";
|
||||
import type { QrCodeScanResult } from "@/api/auth";
|
||||
|
||||
definePage({
|
||||
@@ -61,8 +62,8 @@ const scanResult = ref<QrCodeScanResult | null>(null);
|
||||
const markScanned = async (code: string) => {
|
||||
try {
|
||||
scanResult.value = await userStore.qrScan(code);
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || "二维码无效或已过期");
|
||||
} catch (error) {
|
||||
toast.error(getErrorMessage(error, "二维码无效或已过期"));
|
||||
setTimeout(() => uni.navigateBack(), TOAST_DURATION);
|
||||
}
|
||||
};
|
||||
@@ -75,8 +76,8 @@ const handleConfirm = async () => {
|
||||
await userStore.qrConfirm(ticket.value);
|
||||
toast.success("已确认登录");
|
||||
setTimeout(() => uni.navigateBack(), TOAST_DURATION);
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || "确认失败");
|
||||
} catch (error) {
|
||||
toast.error(getErrorMessage(error, "确认失败"));
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
@@ -96,7 +97,7 @@ const handleCancel = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
onLoad((options: any) => {
|
||||
onLoad((options) => {
|
||||
const code = options?.ticket ? decodeURIComponent(options.ticket) : "";
|
||||
if (!code) {
|
||||
toast.error("二维码无效");
|
||||
|
||||
@@ -67,9 +67,7 @@
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="mt-32rpx">
|
||||
<wd-button plain size="large" block @click="handleResetTheme">
|
||||
重置为默认主题
|
||||
</wd-button>
|
||||
<wd-button plain size="large" block @click="handleResetTheme">重置为默认主题</wd-button>
|
||||
</view>
|
||||
|
||||
<!-- 自定义颜色输入弹窗 -->
|
||||
|
||||
@@ -15,8 +15,8 @@ definePage({
|
||||
|
||||
const url = ref("");
|
||||
|
||||
onLoad((query: any) => {
|
||||
if (query.url) {
|
||||
onLoad((query) => {
|
||||
if (query?.url) {
|
||||
url.value = decodeURIComponent(query.url);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -72,9 +72,7 @@
|
||||
</wd-form>
|
||||
<view class="popup-actions">
|
||||
<wd-button type="info" variant="plain" @click="closeConfigDialog">取消</wd-button>
|
||||
<wd-button :loading="isSubmitting" @click="submitConfigForm">
|
||||
保存
|
||||
</wd-button>
|
||||
<wd-button :loading="isSubmitting" @click="submitConfigForm">保存</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
@@ -154,7 +152,7 @@ function loadConfigList() {
|
||||
function fetchConfigList() {
|
||||
loadMoreState.value = "loading";
|
||||
ConfigAPI.getPage(queryParams)
|
||||
.then((data: any) => {
|
||||
.then((data) => {
|
||||
pageData.value = data.list;
|
||||
total.value = data.total;
|
||||
queryParams.pageNum++;
|
||||
@@ -205,7 +203,7 @@ function closeConfigDialog() {
|
||||
}
|
||||
|
||||
// 操作菜单分发
|
||||
function handleActionSelect({ item }: { item: any }) {
|
||||
function handleActionSelect({ item }: { item: { name: string } }) {
|
||||
const value = item.name;
|
||||
const configItem = currentActionItem.value;
|
||||
if (value === "编辑") {
|
||||
|
||||
@@ -104,9 +104,10 @@ import { onLoad } from "@dcloudio/uni-app";
|
||||
import { toFormSchema } from "@/utils/form";
|
||||
import { findOptionChain } from "@/utils/tree";
|
||||
import { useToast, useDialog } from "@wot-ui/ui";
|
||||
import type { CascaderOption } from "@wot-ui/ui/components/wd-cascader/types";
|
||||
import DeptAPI, { type DeptQuery, DeptItem, DeptForm } from "@/api/dept";
|
||||
import { hasPermission } from "@/utils/permission";
|
||||
import CustomTree from "@/components/custom-tree/index.vue";
|
||||
import CustomTree, { type TreeOption } from "@/components/custom-tree/index.vue";
|
||||
|
||||
definePage({
|
||||
name: "dept",
|
||||
@@ -140,10 +141,10 @@ const formData = reactive<DeptForm>({ ...initialFormData });
|
||||
// 转换为树组件数据格式
|
||||
const treeData = computed(() => deptList.value.map((dept) => transformDeptToTree(dept)));
|
||||
|
||||
function transformDeptToTree(dept: DeptItem): any {
|
||||
function transformDeptToTree(dept: DeptItem): TreeOption {
|
||||
return {
|
||||
value: String(dept.id),
|
||||
label: dept.name,
|
||||
label: dept.name ?? "",
|
||||
id: dept.id,
|
||||
name: dept.name,
|
||||
status: dept.status,
|
||||
@@ -158,10 +159,16 @@ const parentOptions = ref<OptionType[]>([]);
|
||||
const parentLabel = ref("");
|
||||
|
||||
// 上级部门确认选择
|
||||
const handleParentConfirm = ({ value, selectedOptions }: any) => {
|
||||
const handleParentConfirm = ({
|
||||
value,
|
||||
selectedOptions,
|
||||
}: {
|
||||
value: string | number;
|
||||
selectedOptions: CascaderOption[];
|
||||
}) => {
|
||||
parentSelected.value = value;
|
||||
formData.parentId = Number(value) || 0;
|
||||
parentLabel.value = selectedOptions.map((item: any) => item.label).join("/");
|
||||
parentLabel.value = selectedOptions.map((item) => String(item.label ?? "")).join("/");
|
||||
};
|
||||
|
||||
const rules = toFormSchema({
|
||||
@@ -185,7 +192,7 @@ function loadDeptList() {
|
||||
}
|
||||
|
||||
// 操作菜单分发
|
||||
function handleActionSelect({ item }: { item: any }) {
|
||||
function handleActionSelect({ item }: { item: { name: string } }) {
|
||||
const value = item.name;
|
||||
const dept = currentActionItem.value;
|
||||
if (value === "新增子部门") {
|
||||
@@ -206,7 +213,7 @@ function handleActionSelect({ item }: { item: any }) {
|
||||
}
|
||||
|
||||
// 处理节点操作按钮点击
|
||||
function handleNodeAction(node: any) {
|
||||
function handleNodeAction(node: TreeOption) {
|
||||
const dept: DeptItem = {
|
||||
id: node.id,
|
||||
name: node.label,
|
||||
@@ -272,7 +279,9 @@ function handleAddChild(dept: DeptItem) {
|
||||
formData.parentId = dept.id!;
|
||||
parentSelected.value = dept.id!;
|
||||
parentLabel.value =
|
||||
findOptionChain(treeData.value, dept.id!)?.map((option) => option.label).join("/") ||
|
||||
findOptionChain(treeData.value, dept.id!)
|
||||
?.map((option) => option.label)
|
||||
.join("/") ||
|
||||
dept.name ||
|
||||
"";
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ function loadDictTypeList() {
|
||||
function fetchDictTypeList() {
|
||||
loadMoreState.value = "loading";
|
||||
DictAPI.getPage(queryParams)
|
||||
.then((data: any) => {
|
||||
.then((data) => {
|
||||
pageData.value = data.list;
|
||||
total.value = data.total;
|
||||
queryParams.pageNum++;
|
||||
@@ -249,7 +249,7 @@ function showDictActions(item: DictTypeItem) {
|
||||
actionSheetVisible.value = true;
|
||||
}
|
||||
|
||||
function handleActionSelect({ item }: { item: any }) {
|
||||
function handleActionSelect({ item }: { item: { name: string } }) {
|
||||
pendingAction.value[item.name]?.();
|
||||
}
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ function fetchItemList() {
|
||||
|
||||
loadMoreState.value = "loading";
|
||||
DictAPI.getItemPage(dictCode.value, queryParams)
|
||||
.then((data: any) => {
|
||||
.then((data) => {
|
||||
pageData.value = data.list;
|
||||
total.value = data.total;
|
||||
queryParams.pageNum++;
|
||||
@@ -264,7 +264,7 @@ function showItemActions(item: DictDataItem) {
|
||||
actionSheetVisible.value = true;
|
||||
}
|
||||
|
||||
function handleActionSelect({ item }: { item: any }) {
|
||||
function handleActionSelect({ item }: { item: { name: string } }) {
|
||||
pendingAction.value[item.name]?.();
|
||||
}
|
||||
|
||||
@@ -276,9 +276,9 @@ onReachBottom(() => {
|
||||
}
|
||||
});
|
||||
|
||||
onLoad((query: any) => {
|
||||
dictCode.value = String((query as any)?.dictCode || "");
|
||||
pageTitle.value = String((query as any)?.title || "字典数据");
|
||||
onLoad((query) => {
|
||||
dictCode.value = String(query?.dictCode || "");
|
||||
pageTitle.value = String(query?.title || "字典数据");
|
||||
if (pageTitle.value) {
|
||||
uni.setNavigationBarTitle({ title: pageTitle.value });
|
||||
}
|
||||
|
||||
@@ -81,7 +81,11 @@
|
||||
<wd-radio value="B">按钮</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-form-item>
|
||||
<wd-form-item v-if="formData.type === 'C' || formData.type === 'M'" prop="routePath" title="路由路径">
|
||||
<wd-form-item
|
||||
v-if="formData.type === 'C' || formData.type === 'M'"
|
||||
prop="routePath"
|
||||
title="路由路径"
|
||||
>
|
||||
<wd-input v-model="formData.routePath" placeholder="system 或 /system" />
|
||||
</wd-form-item>
|
||||
<wd-form-item v-if="formData.type === 'E'" prop="externalUrl" title="外链地址">
|
||||
@@ -136,7 +140,8 @@ import { findOptionChain } from "@/utils/tree";
|
||||
import { useToast, useDialog } from "@wot-ui/ui";
|
||||
import MenuAPI, { type MenuQuery, MenuItem, MenuForm } from "@/api/menu";
|
||||
import { hasPermission } from "@/utils/permission";
|
||||
import CustomTree from "@/components/custom-tree/index.vue";
|
||||
import CustomTree, { type TreeOption } from "@/components/custom-tree/index.vue";
|
||||
import type { CascaderOption } from "@wot-ui/ui/components/wd-cascader/types";
|
||||
|
||||
definePage({
|
||||
name: "menu",
|
||||
@@ -175,10 +180,10 @@ const formData = reactive<MenuForm>({ ...initialFormData });
|
||||
// 转换为树组件数据格式
|
||||
const treeData = computed(() => menuList.value.map((menu: MenuItem) => transformMenuToTree(menu)));
|
||||
|
||||
function transformMenuToTree(menu: MenuItem): any {
|
||||
function transformMenuToTree(menu: MenuItem): TreeOption {
|
||||
return {
|
||||
value: menu.id,
|
||||
label: menu.name,
|
||||
value: menu.id ?? "",
|
||||
label: menu.name ?? "",
|
||||
id: menu.id,
|
||||
name: menu.name,
|
||||
type: menu.type,
|
||||
@@ -221,10 +226,16 @@ const parentOptions = ref<OptionType[]>([]);
|
||||
const parentLabel = ref("");
|
||||
|
||||
// 上级菜单确认选择
|
||||
const handleParentConfirm = ({ value, selectedOptions }: any) => {
|
||||
const handleParentConfirm = ({
|
||||
value,
|
||||
selectedOptions,
|
||||
}: {
|
||||
value: string | number;
|
||||
selectedOptions: CascaderOption[];
|
||||
}) => {
|
||||
parentSelected.value = value;
|
||||
formData.parentId = String(value) || "0";
|
||||
parentLabel.value = selectedOptions.map((item: any) => item.label).join("/");
|
||||
parentLabel.value = selectedOptions.map((item) => String(item.label ?? "")).join("/");
|
||||
};
|
||||
|
||||
const rules = toFormSchema({
|
||||
@@ -236,13 +247,13 @@ const rules = toFormSchema({
|
||||
const handleSearch = () => loadMenuList();
|
||||
|
||||
function loadMenuList() {
|
||||
MenuAPI.getList(queryParams).then((data: any) => {
|
||||
MenuAPI.getList(queryParams).then((data) => {
|
||||
menuList.value = data;
|
||||
});
|
||||
}
|
||||
|
||||
// 操作菜单分发
|
||||
function handleActionSelect({ item }: { item: any }) {
|
||||
function handleActionSelect({ item }: { item: { name: string } }) {
|
||||
const value = item.name;
|
||||
const menu = currentActionItem.value;
|
||||
if (value === "新增子菜单") {
|
||||
@@ -263,7 +274,7 @@ function handleActionSelect({ item }: { item: any }) {
|
||||
}
|
||||
|
||||
// 处理节点操作按钮点击
|
||||
function handleNodeAction(node: any) {
|
||||
function handleNodeAction(node: TreeOption) {
|
||||
const menu: MenuItem = {
|
||||
id: node.id,
|
||||
name: node.label,
|
||||
@@ -330,7 +341,9 @@ function handleAddChild(menu: MenuItem) {
|
||||
formData.parentId = menu.id!;
|
||||
parentSelected.value = menu.id!;
|
||||
parentLabel.value =
|
||||
findOptionChain(treeData.value, menu.id!)?.map((option) => option.label).join("/") ||
|
||||
findOptionChain(treeData.value, menu.id!)
|
||||
?.map((option) => option.label)
|
||||
.join("/") ||
|
||||
menu.name ||
|
||||
"";
|
||||
}
|
||||
|
||||
@@ -134,9 +134,7 @@
|
||||
</wd-form>
|
||||
<view class="popup-actions">
|
||||
<wd-button type="info" variant="plain" @click="closeNoticeForm">取消</wd-button>
|
||||
<wd-button :loading="isSubmitting" @click="submitNoticeForm">
|
||||
保存
|
||||
</wd-button>
|
||||
<wd-button :loading="isSubmitting" @click="submitNoticeForm">保存</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
@@ -390,7 +388,7 @@ function showNoticeActions(item: NoticeItem) {
|
||||
actionSheetVisible.value = true;
|
||||
}
|
||||
|
||||
function handleActionSelect({ item }: { item: any }) {
|
||||
function handleActionSelect({ item }: { item: { name: string } }) {
|
||||
pendingAction.value[item.name]?.();
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
<view class="action-bar">
|
||||
<view class="action-bar__left">
|
||||
<wd-button type="" size="small" variant="plain" @click="handleSelectAll">全选</wd-button>
|
||||
<wd-button type="" size="small" variant="plain" @click="handleClearAll">取消全选</wd-button>
|
||||
<wd-button type="" size="small" variant="plain" @click="handleClearAll">
|
||||
取消全选
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -254,7 +254,7 @@ function closeRoleDialog() {
|
||||
}
|
||||
|
||||
// 操作菜单分发
|
||||
function handleActionSelect({ item }: { item: any }) {
|
||||
function handleActionSelect({ item }: { item: { name: string } }) {
|
||||
const value = item.name;
|
||||
const role = currentActionItem.value;
|
||||
if (value === "编辑") {
|
||||
@@ -267,7 +267,7 @@ function handleActionSelect({ item }: { item: any }) {
|
||||
msg: `确定要删除角色「${role.name}」吗?`,
|
||||
headerImage: "warning",
|
||||
}).then(async () => {
|
||||
await RoleAPI.deleteByIds(String(item.id));
|
||||
await RoleAPI.deleteByIds(String(role.id));
|
||||
toast.success("删除成功");
|
||||
loadRoleList();
|
||||
});
|
||||
|
||||
@@ -202,10 +202,7 @@
|
||||
<wd-button type="info" variant="plain" @click="resetPwdDialog.visible = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button
|
||||
:loading="resetPwdDialog.isSubmitting"
|
||||
@click="handleResetPassword"
|
||||
>
|
||||
<wd-button :loading="resetPwdDialog.isSubmitting" @click="handleResetPassword">
|
||||
确认
|
||||
</wd-button>
|
||||
</view>
|
||||
@@ -217,6 +214,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { onLoad, onReachBottom } from "@dcloudio/uni-app";
|
||||
import { LoadMoreState } from "@wot-ui/ui/components/wd-loadmore/types";
|
||||
import type { CascaderOption } from "@wot-ui/ui/components/wd-cascader/types";
|
||||
import { toFormSchema } from "@/utils/form";
|
||||
import { findOptionChain } from "@/utils/tree";
|
||||
import { useQueue, useToast, useDialog } from "@wot-ui/ui";
|
||||
@@ -261,7 +259,7 @@ const initialFormData: UserForm = {
|
||||
};
|
||||
|
||||
const formData = reactive<UserForm>({ ...initialFormData });
|
||||
const roleOptions = ref<Record<string, any>[]>([]);
|
||||
const roleOptions = ref<OptionType[]>([]);
|
||||
const deptOptions = ref<OptionType[]>([]);
|
||||
|
||||
// 部门选择器状态
|
||||
@@ -274,14 +272,25 @@ const showRolePicker = ref(false);
|
||||
const roleLabel = ref("");
|
||||
|
||||
// 部门确认选择
|
||||
const handleDeptConfirm = ({ value, selectedOptions }: any) => {
|
||||
formData.deptId = value;
|
||||
deptLabel.value = selectedOptions.map((item: any) => item.label).join("/");
|
||||
const handleDeptConfirm = ({
|
||||
value,
|
||||
selectedOptions,
|
||||
}: {
|
||||
value: string | number;
|
||||
selectedOptions: CascaderOption[];
|
||||
}) => {
|
||||
formData.deptId = Number(value);
|
||||
deptLabel.value = selectedOptions.map((item) => String(item.label ?? "")).join("/");
|
||||
};
|
||||
|
||||
// 角色确认选择
|
||||
const handleRoleConfirm = ({ selectedItems }: any) => {
|
||||
roleLabel.value = selectedItems.map((item: any) => item.label).join("、");
|
||||
const handleRoleConfirm = ({
|
||||
selectedItems,
|
||||
}: {
|
||||
value: (string | number)[];
|
||||
selectedItems: OptionType[];
|
||||
}) => {
|
||||
roleLabel.value = selectedItems.map((item) => item.label).join("、");
|
||||
};
|
||||
|
||||
const rules = toFormSchema({
|
||||
@@ -336,7 +345,7 @@ function resetUserFilter() {
|
||||
function fetchUserList() {
|
||||
loadMoreState.value = "loading";
|
||||
UserAPI.getPage(queryParams)
|
||||
.then((data: any) => {
|
||||
.then((data) => {
|
||||
pageData.value = data.list;
|
||||
total.value = data.total;
|
||||
queryParams.pageNum++;
|
||||
@@ -371,11 +380,12 @@ async function openUserDialog(user?: UserItem) {
|
||||
const chain = findOptionChain(deptData, data.deptId);
|
||||
deptLabel.value = chain ? chain.map((option) => option.label).join("/") : user.deptName || "";
|
||||
}
|
||||
// 回显角色
|
||||
// 回显角色(roleIds 与选项 value 统一转字符串比较,兼容数字/字符串ID)
|
||||
if (data.roleIds && data.roleIds.length > 0) {
|
||||
const roleIds = data.roleIds.map(String);
|
||||
const names = roleOptions.value
|
||||
.filter((r: any) => data.roleIds.includes(r.value))
|
||||
.map((r: any) => r.label);
|
||||
.filter((r) => roleIds.includes(String(r.value)))
|
||||
.map((r) => r.label);
|
||||
roleLabel.value = names.join("、");
|
||||
}
|
||||
}
|
||||
@@ -464,7 +474,7 @@ function showUserActions(item: UserItem) {
|
||||
actionSheetVisible.value = true;
|
||||
}
|
||||
|
||||
function handleActionSelect({ item }: { item: any }) {
|
||||
function handleActionSelect({ item }: { item: { name: string } }) {
|
||||
pendingAction.value[item.name]?.();
|
||||
}
|
||||
|
||||
|
||||
+16
-3
@@ -3,9 +3,22 @@ import { getAccessToken } from "@/utils/auth";
|
||||
import { createRouter } from "uni-mini-router";
|
||||
import { useUserStore } from "@/store";
|
||||
|
||||
/** virtual:uni-pages 生成的页面路由描述 */
|
||||
interface PageMetaRoute {
|
||||
path: string;
|
||||
meta?: Record<string, unknown>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/** 分包路由描述 */
|
||||
interface SubPackageRoute {
|
||||
root: string;
|
||||
pages: PageMetaRoute[];
|
||||
}
|
||||
|
||||
// 生成路由配置
|
||||
function generateRoutes() {
|
||||
const routes = pages.map((page: { path: string; [key: string]: any }) => {
|
||||
const routes = pages.map((page: PageMetaRoute) => {
|
||||
const newPath = `/${page.path}`;
|
||||
const meta = page.meta ?? undefined;
|
||||
return { ...page, path: newPath, meta };
|
||||
@@ -13,8 +26,8 @@ function generateRoutes() {
|
||||
|
||||
// 处理分包路由
|
||||
if (subPackages && subPackages.length > 0) {
|
||||
subPackages.forEach((subPackage: { root: string; pages: any[] }) => {
|
||||
const subRoutes = subPackage.pages.map((page: any) => {
|
||||
subPackages.forEach((subPackage: SubPackageRoute) => {
|
||||
const subRoutes = subPackage.pages.map((page) => {
|
||||
const newPath = `/${subPackage.root}/${page.path}`;
|
||||
const meta = page.meta ?? undefined;
|
||||
return { ...page, path: newPath, meta };
|
||||
|
||||
Vendored
+6
@@ -37,12 +37,14 @@ declare global {
|
||||
const dict: typeof import('../api/dict')['default']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const file: typeof import('../api/file')['default']
|
||||
const findOptionChain: typeof import('../utils/tree')['findOptionChain']
|
||||
const formatBytes: typeof import('../utils/format')['formatBytes']
|
||||
const formatNumber: typeof import('../utils/format')['formatNumber']
|
||||
const getAccessToken: typeof import('../utils/auth')['getAccessToken']
|
||||
const getActivePinia: typeof import('pinia')['getActivePinia']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const getErrorMessage: typeof import('../utils/index')['getErrorMessage']
|
||||
const getRefreshToken: typeof import('../utils/auth')['getRefreshToken']
|
||||
const getToken: typeof import('../utils/storage')['getToken']
|
||||
const getUserInfo: typeof import('../utils/storage')['getUserInfo']
|
||||
@@ -188,6 +190,7 @@ declare global {
|
||||
const useRoute: typeof import('uni-mini-router')['useRoute']
|
||||
const useRouter: typeof import('uni-mini-router')['useRouter']
|
||||
const useScanCode: typeof import('@uni-helper/uni-use')['useScanCode']
|
||||
const useScanLogin: typeof import('../composables/useScanLogin')['useScanLogin']
|
||||
const useScreenBrightness: typeof import('@uni-helper/uni-use')['useScreenBrightness']
|
||||
const useSelectorQuery: typeof import('@uni-helper/uni-use')['useSelectorQuery']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
@@ -253,12 +256,14 @@ declare module 'vue' {
|
||||
readonly dict: UnwrapRef<typeof import('../api/dict')['default']>
|
||||
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
|
||||
readonly file: UnwrapRef<typeof import('../api/file')['default']>
|
||||
readonly findOptionChain: UnwrapRef<typeof import('../utils/tree')['findOptionChain']>
|
||||
readonly formatBytes: UnwrapRef<typeof import('../utils/format')['formatBytes']>
|
||||
readonly formatNumber: UnwrapRef<typeof import('../utils/format')['formatNumber']>
|
||||
readonly getAccessToken: UnwrapRef<typeof import('../utils/auth')['getAccessToken']>
|
||||
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
|
||||
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
||||
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||
readonly getErrorMessage: UnwrapRef<typeof import('../utils/index')['getErrorMessage']>
|
||||
readonly getRefreshToken: UnwrapRef<typeof import('../utils/auth')['getRefreshToken']>
|
||||
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||
readonly hasAllPermissions: UnwrapRef<typeof import('../utils/permission')['hasAllPermissions']>
|
||||
@@ -360,6 +365,7 @@ declare module 'vue' {
|
||||
readonly useRequest: UnwrapRef<typeof import('../composables/useRequest')['useRequest']>
|
||||
readonly useRoute: UnwrapRef<typeof import('uni-mini-router')['useRoute']>
|
||||
readonly useRouter: UnwrapRef<typeof import('uni-mini-router')['useRouter']>
|
||||
readonly useScanLogin: UnwrapRef<typeof import('../composables/useScanLogin')['useScanLogin']>
|
||||
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
||||
readonly useSse: UnwrapRef<typeof import('../composables/useSse')['useSse']>
|
||||
readonly useTabbar: UnwrapRef<typeof import('../composables/useTabbar')['useTabbar']>
|
||||
|
||||
Vendored
+1
-1
@@ -7,7 +7,6 @@ type _LocationUrl =
|
||||
"/pages/index/index" |
|
||||
"/pages/login/index" |
|
||||
"/pages/mine/index" |
|
||||
"/pages/mine/scan-confirm/index" |
|
||||
"/pages/webview/index" |
|
||||
"/pages/work/index" |
|
||||
"/pages/mine/about/index" |
|
||||
@@ -15,6 +14,7 @@ type _LocationUrl =
|
||||
"/pages/mine/official/index" |
|
||||
"/pages/mine/profile/complete-profile" |
|
||||
"/pages/mine/profile/index" |
|
||||
"/pages/mine/scan-confirm/index" |
|
||||
"/pages/mine/settings/index" |
|
||||
"/pages/work/config/index" |
|
||||
"/pages/work/dept/index" |
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ function getCurrentPagePath(): string {
|
||||
|
||||
const currentPage = pages[pages.length - 1];
|
||||
const route = currentPage.route || "";
|
||||
const options = (currentPage as any).options || {};
|
||||
const options = (currentPage as { options?: Record<string, string> }).options || {};
|
||||
|
||||
const query = Object.entries(options)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
|
||||
+24
-2
@@ -4,9 +4,9 @@
|
||||
* @param delay 延迟时间
|
||||
* @returns
|
||||
*/
|
||||
const debounce = <T extends (...args: any[]) => any>(fn: T, delay: number) => {
|
||||
const debounce = <T extends (...args: any[]) => unknown>(fn: T, delay: number) => {
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
return function (this: any, ...args: Parameters<T>) {
|
||||
return function (this: unknown, ...args: Parameters<T>) {
|
||||
if (timer !== null) clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
fn.apply(this, args);
|
||||
@@ -16,3 +16,25 @@ const debounce = <T extends (...args: any[]) => any>(fn: T, delay: number) => {
|
||||
};
|
||||
|
||||
export { debounce };
|
||||
|
||||
/**
|
||||
* 从 catch 捕获的 unknown 错误中提取提示文案
|
||||
* @param error catch 捕获的错误对象
|
||||
* @param fallback 无有效信息时的兜底文案
|
||||
* @returns 错误提示文案
|
||||
*/
|
||||
export function getErrorMessage(error: unknown, fallback: string): string {
|
||||
if (error instanceof Error && error.message) return error.message;
|
||||
if (typeof error === "string" && error) return error;
|
||||
if (typeof error === "object" && error !== null) {
|
||||
const { message, msg, errMsg } = error as {
|
||||
message?: unknown;
|
||||
msg?: unknown;
|
||||
errMsg?: unknown;
|
||||
};
|
||||
if (typeof message === "string" && message) return message;
|
||||
if (typeof msg === "string" && msg) return msg;
|
||||
if (typeof errMsg === "string" && errMsg) return errMsg;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
@@ -68,9 +68,13 @@ function request<T = any>(options: RequestOptions): Promise<T> {
|
||||
header,
|
||||
timeout: options.timeout || 30000,
|
||||
responseType: options.responseType,
|
||||
success: (res: any) => {
|
||||
const serverCode = res?.data?.code;
|
||||
const serverMsg = res?.data?.msg || res?.data?.message;
|
||||
success: (res: UniApp.RequestSuccessCallbackResult) => {
|
||||
// 后端统一响应体(data 在泛型 T 下无法精确描述,这里只取业务层字段)
|
||||
const body = res?.data as
|
||||
| { code?: string; msg?: string; message?: string; data?: unknown }
|
||||
| undefined;
|
||||
const serverCode = body?.code;
|
||||
const serverMsg = body?.msg || body?.message;
|
||||
|
||||
// 令牌无效/过期(业务码 A023x 或 HTTP 401):清除 token 并跳转登录页(防抖)
|
||||
const isTokenError =
|
||||
@@ -94,7 +98,7 @@ function request<T = any>(options: RequestOptions): Promise<T> {
|
||||
// HTTP 成功:校验业务码
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
if (!serverCode || serverCode === ApiCode.SUCCESS) {
|
||||
resolve(res.data.data);
|
||||
resolve(body?.data as T);
|
||||
} else {
|
||||
reject(new RequestError(serverMsg || "请求失败", res.statusCode, serverCode));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user