style: 代码格式化与统一样式变量

This commit is contained in:
Ray.Hao
2026-04-18 00:08:06 +08:00
parent 012362f61d
commit 79ebad0aa1
28 changed files with 744 additions and 532 deletions

View File

@@ -80,7 +80,7 @@ const AuthAPI = {
*/
sendSmsLoginCode(mobile: string): Promise<void> {
const mobileSafe = encodeURIComponent(mobile);
return request<void>({
return request({
url: `${AUTH_BASE_URL}/sms/code?mobile=${mobileSafe}`,
method: "POST",
});
@@ -153,7 +153,7 @@ const AuthAPI = {
* 登出
*/
logout() {
return request<void>({
return request({
url: `${AUTH_BASE_URL}/logout`,
method: "DELETE",
});

View File

@@ -211,8 +211,6 @@ function handleNodeAction(node: FlatNode) {
emit("action", node.raw);
}
// ===== 多选相关 =====
/** 判断节点是否选中(多选模式) */
function isChecked(value: string) {
return internalCheckedKeys.value.has(value);

View File

@@ -1,4 +1,4 @@
import { ref, computed } from "vue"
import { ref, computed } from "vue";
/**
* 加载状态管理
@@ -26,63 +26,63 @@ import { ref, computed } from "vue"
export interface UseLoadingOptions {
/** 是否使用全局单例,默认 true */
singleton?: boolean
singleton?: boolean;
/** 初始加载状态,默认 false */
initial?: boolean
initial?: boolean;
/** 加载提示消息 */
message?: string
message?: string;
}
export interface UseLoadingReturn {
/** 是否处于加载状态 */
isLoading: ReturnType<typeof computed<boolean>>
isLoading: ReturnType<typeof computed<boolean>>;
/** 加载计数(支持并发) */
loadingCount: ReturnType<typeof ref<number>>
loadingCount: ReturnType<typeof ref<number>>;
/** 当前加载消息 */
message: ReturnType<typeof ref<string>>
message: ReturnType<typeof ref<string>>;
/** 开始加载 */
start: (msg?: string) => void
start: (msg?: string) => void;
/** 结束加载 */
stop: () => void
stop: () => void;
/** 包装异步函数,自动管理加载状态 */
wrap: <T>(promise: Promise<T>) => Promise<T>
wrap: <T>(promise: Promise<T>) => Promise<T>;
/** 设置加载消息 */
setMessage: (msg: string) => void
setMessage: (msg: string) => void;
}
/**
* 创建加载状态实例
*/
function createLoadingState(options: UseLoadingOptions = {}) {
const { initial = false, message: initialMessage = "" } = options
const { message: initialMessage = "" } = options;
const loadingCount = ref(0)
const message = ref(initialMessage)
const loadingCount = ref(0);
const message = ref(initialMessage);
const isLoading = computed(() => loadingCount.value > 0)
const isLoading = computed(() => loadingCount.value > 0);
/**
* 开始加载
* @param msg 加载提示消息
*/
const start = (msg?: string): void => {
loadingCount.value++
loadingCount.value++;
if (msg) {
message.value = msg
message.value = msg;
}
}
};
/**
* 结束加载
*/
const stop = (): void => {
if (loadingCount.value > 0) {
loadingCount.value--
loadingCount.value--;
}
if (loadingCount.value === 0) {
message.value = ""
message.value = "";
}
}
};
/**
* 包装异步函数,自动管理加载状态
@@ -90,20 +90,20 @@ function createLoadingState(options: UseLoadingOptions = {}) {
*/
const wrap = async <T>(promise: Promise<T>): Promise<T> => {
try {
start()
return await promise
start();
return await promise;
} finally {
stop()
stop();
}
}
};
/**
* 设置加载消息
* @param msg 消息内容
*/
const setMessage = (msg: string): void => {
message.value = msg
}
message.value = msg;
};
return {
isLoading,
@@ -113,11 +113,11 @@ function createLoadingState(options: UseLoadingOptions = {}) {
stop,
wrap,
setMessage,
}
};
}
/** 全局单例实例 */
let loadingInstance: UseLoadingReturn | null = null
let loadingInstance: UseLoadingReturn | null = null;
/**
* 加载状态管理 Hook
@@ -128,14 +128,14 @@ let loadingInstance: UseLoadingReturn | null = null
* @param options 配置选项
*/
export function useLoading(options?: UseLoadingOptions): UseLoadingReturn {
const { singleton = true } = options ?? {}
const { singleton = true } = options ?? {};
if (singleton) {
if (!loadingInstance) {
loadingInstance = createLoadingState(options)
loadingInstance = createLoadingState(options);
}
return loadingInstance
return loadingInstance;
}
return createLoadingState(options)
return createLoadingState(options);
}

View File

@@ -72,7 +72,7 @@ export interface UseNavbarReturn {
* @param options 配置选项
*/
export function useNavbar(options: UseNavbarOptions = {}): UseNavbarReturn {
const { navBarHeight = 44, hasTabbar = false } = options;
const { navBarHeight = 44 } = options;
// 状态栏高度
const statusBarHeight = ref(0);

View File

@@ -1,4 +1,4 @@
import { ref, computed, shallowRef, type Ref, type ComputedRef } from "vue"
import { ref, computed, shallowRef, type Ref, type ComputedRef } from "vue";
/**
* 请求状态管理
@@ -24,36 +24,36 @@ import { ref, computed, shallowRef, type Ref, type ComputedRef } from "vue"
export interface UseRequestOptions<T> {
/** 是否立即执行,默认 false */
immediate?: boolean
immediate?: boolean;
/** 初始数据 */
initialData?: T | null
initialData?: T | null;
/** 请求前的回调 */
onBefore?: () => void
onBefore?: () => void;
/** 请求成功的回调 */
onSuccess?: (data: T) => void
onSuccess?: (data: T) => void;
/** 请求失败的回调 */
onError?: (error: Error) => void
onError?: (error: Error) => void;
/** 请求完成的回调(无论成功失败) */
onFinally?: () => void
onFinally?: () => void;
/** 是否重置数据在重新请求时,默认 false */
resetOnExecute?: boolean
resetOnExecute?: boolean;
}
export interface UseRequestReturn<T> {
/** 响应数据 */
data: Ref<T | null>
data: Ref<T | null>;
/** 是否正在加载 */
loading: ComputedRef<boolean>
loading: ComputedRef<boolean>;
/** 错误信息 */
error: Ref<Error | null>
error: Ref<Error | null>;
/** 是否已请求过(用于区分初始状态和请求后状态) */
hasExecuted: Ref<boolean>
hasExecuted: Ref<boolean>;
/** 执行请求 */
execute: () => Promise<T | null>
execute: () => Promise<T | null>;
/** 刷新数据(重新执行请求) */
refresh: () => Promise<T | null>
refresh: () => Promise<T | null>;
/** 重置状态 */
reset: () => void
reset: () => void;
}
/**
@@ -74,15 +74,15 @@ export function useRequest<T>(
onError,
onFinally,
resetOnExecute = false,
} = options
} = options;
// 使用 shallowRef 避免深层响应式,提升性能
const data = shallowRef<T | null>(initialData) as Ref<T | null>
const error = ref<Error | null>(null)
const loadingCount = ref(0)
const hasExecuted = ref(false)
const data = shallowRef<T | null>(initialData) as Ref<T | null>;
const error = ref<Error | null>(null);
const loadingCount = ref(0);
const hasExecuted = ref(false);
const loading = computed(() => loadingCount.value > 0)
const loading = computed(() => loadingCount.value > 0);
/**
* 执行请求
@@ -90,51 +90,51 @@ export function useRequest<T>(
const execute = async (): Promise<T | null> => {
// 重置状态
if (resetOnExecute) {
data.value = initialData
error.value = null
data.value = initialData;
error.value = null;
}
loadingCount.value++
hasExecuted.value = true
loadingCount.value++;
hasExecuted.value = true;
try {
onBefore?.()
const result = await requestFn()
data.value = result
error.value = null
onSuccess?.(result)
return result
onBefore?.();
const result = await requestFn();
data.value = result;
error.value = null;
onSuccess?.(result);
return result;
} catch (e) {
const err = e instanceof Error ? e : new Error(String(e))
error.value = err
onError?.(err)
return null
const err = e instanceof Error ? e : new Error(String(e));
error.value = err;
onError?.(err);
return null;
} finally {
loadingCount.value--
onFinally?.()
loadingCount.value--;
onFinally?.();
}
}
};
/**
* 刷新数据
*/
const refresh = async (): Promise<T | null> => {
return execute()
}
return execute();
};
/**
* 重置状态
*/
const reset = (): void => {
data.value = initialData
error.value = null
loadingCount.value = 0
hasExecuted.value = false
}
data.value = initialData;
error.value = null;
loadingCount.value = 0;
hasExecuted.value = false;
};
// 立即执行
if (immediate) {
execute()
execute();
}
return {
@@ -145,7 +145,7 @@ export function useRequest<T>(
execute,
refresh,
reset,
}
};
}
/**
@@ -167,42 +167,42 @@ export function useRequest<T>(
*/
export interface PaginationParams {
pageNum: number
pageSize: number
pageNum: number;
pageSize: number;
}
export interface UsePaginationOptions<T> {
/** 每页条数,默认 10 */
pageSize?: number
pageSize?: number;
/** 初始数据 */
initialData?: T[]
initialData?: T[];
/** 请求成功的回调 */
onSuccess?: (list: T[], total: number) => void
onSuccess?: (list: T[], total: number) => void;
/** 请求失败的回调 */
onError?: (error: Error) => void
onError?: (error: Error) => void;
}
export interface UsePaginationReturn<T> {
/** 数据列表 */
list: Ref<T[]>
list: Ref<T[]>;
/** 是否正在加载 */
loading: ComputedRef<boolean>
loading: ComputedRef<boolean>;
/** 错误信息 */
error: Ref<Error | null>
error: Ref<Error | null>;
/** 是否还有更多数据 */
hasMore: ComputedRef<boolean>
hasMore: ComputedRef<boolean>;
/** 当前页码 */
pageNum: Ref<number>
pageNum: Ref<number>;
/** 总条数 */
total: Ref<number>
total: Ref<number>;
/** 是否为空 */
isEmpty: ComputedRef<boolean>
isEmpty: ComputedRef<boolean>;
/** 加载更多 */
loadMore: () => Promise<T[]>
loadMore: () => Promise<T[]>;
/** 刷新(从第一页开始) */
refresh: () => Promise<T[]>
refresh: () => Promise<T[]>;
/** 重置 */
reset: () => void
reset: () => void;
}
/**
@@ -215,17 +215,17 @@ export function usePagination<T>(
requestFn: (params: PaginationParams) => Promise<PageResult<T[]>>,
options: UsePaginationOptions<T> = {}
): UsePaginationReturn<T> {
const { pageSize = 10, initialData = [], onSuccess, onError } = options
const { pageSize = 10, initialData = [], onSuccess, onError } = options;
const list = ref<T[]>(initialData) as Ref<T[]>
const loadingCount = ref(0)
const error = ref<Error | null>(null)
const pageNum = ref(1)
const total = ref(0)
const list = ref<T[]>(initialData) as Ref<T[]>;
const loadingCount = ref(0);
const error = ref<Error | null>(null);
const pageNum = ref(1);
const total = ref(0);
const loading = computed(() => loadingCount.value > 0)
const hasMore = computed(() => list.value.length < total.value)
const isEmpty = computed(() => list.value.length === 0 && !loading.value)
const loading = computed(() => loadingCount.value > 0);
const hasMore = computed(() => list.value.length < total.value);
const isEmpty = computed(() => list.value.length === 0 && !loading.value);
/**
* 加载更多
@@ -233,59 +233,59 @@ export function usePagination<T>(
const loadMore = async (): Promise<T[]> => {
// 如果正在加载或没有更多数据,直接返回
if (loading.value || !hasMore.value) {
return list.value
return list.value;
}
loadingCount.value++
loadingCount.value++;
try {
const result = await requestFn({
pageNum: pageNum.value,
pageSize,
})
});
// 如果是第一页,替换数据;否则追加数据
if (pageNum.value === 1) {
list.value = result.list
list.value = result.list;
} else {
list.value = [...list.value, ...result.list]
list.value = [...list.value, ...result.list];
}
total.value = result.total
pageNum.value++
onSuccess?.(result.list, result.total)
total.value = result.total;
pageNum.value++;
onSuccess?.(result.list, result.total);
return result.list
return result.list;
} catch (e) {
const err = e instanceof Error ? e : new Error(String(e))
error.value = err
onError?.(err)
return []
const err = e instanceof Error ? e : new Error(String(e));
error.value = err;
onError?.(err);
return [];
} finally {
loadingCount.value--
loadingCount.value--;
}
}
};
/**
* 刷新(从第一页开始)
*/
const refresh = async (): Promise<T[]> => {
pageNum.value = 1
total.value = 0
list.value = []
return loadMore()
}
pageNum.value = 1;
total.value = 0;
list.value = [];
return loadMore();
};
/**
* 重置
*/
const reset = (): void => {
list.value = initialData
loadingCount.value = 0
error.value = null
pageNum.value = 1
total.value = 0
}
list.value = initialData;
loadingCount.value = 0;
error.value = null;
pageNum.value = 1;
total.value = 0;
};
return {
list,
@@ -298,5 +298,5 @@ export function usePagination<T>(
loadMore,
refresh,
reset,
}
};
}

View File

@@ -1,4 +1,4 @@
import { ref, computed } from "vue"
import { ref, computed } from "vue";
/**
* TabBar 状态管理
@@ -19,15 +19,15 @@
export interface TabbarItem {
/** 唯一标识 */
name: string
name: string;
/** 徽标数字null 表示不显示 */
value: number | null
value: number | null;
/** 是否激活 */
active: boolean
active: boolean;
/** 显示标题 */
title: string
title: string;
/** 图标名称 */
icon: string
icon: string;
}
/** 默认 TabBar 配置 */
@@ -35,7 +35,7 @@ const DEFAULT_TABBAR_ITEMS: Omit<TabbarItem, "active">[] = [
{ name: "home", value: null, title: "首页", icon: "home" },
{ name: "work", value: null, title: "工作台", icon: "laptop" },
{ name: "mine", value: null, title: "我的", icon: "user" },
]
];
/**
* 创建 TabBar 状态实例
@@ -49,24 +49,24 @@ function createTabbarState() {
...item,
active: index === 0,
}))
)
);
/** TabBar 列表 */
const tabbarList = computed(() => items.value)
const tabbarList = computed(() => items.value);
/** 当前激活的 TabBar 项 */
const activeTabbar = computed(() => {
return items.value.find((item) => item.active) || items.value[0]
})
return items.value.find((item) => item.active) || items.value[0];
});
/**
* 获取指定 TabBar 项的徽标数字
* @param name TabBar 项名称
*/
const getTabbarItemValue = (name: string): number | null => {
const item = items.value.find((item) => item.name === name)
return item?.value ?? null
}
const item = items.value.find((item) => item.name === name);
return item?.value ?? null;
};
/**
* 设置 TabBar 项的徽标数字
@@ -74,11 +74,11 @@ function createTabbarState() {
* @param value 徽标数字
*/
const setTabbarItem = (name: string, value: number): void => {
const item = items.value.find((item) => item.name === name)
const item = items.value.find((item) => item.name === name);
if (item) {
item.value = value
item.value = value;
}
}
};
/**
* 设置激活的 TabBar 项
@@ -86,9 +86,9 @@ function createTabbarState() {
*/
const setTabbarItemActive = (name: string): void => {
items.value.forEach((item) => {
item.active = item.name === name
})
}
item.active = item.name === name;
});
};
/**
* 重置 TabBar 状态
@@ -97,8 +97,8 @@ function createTabbarState() {
items.value = DEFAULT_TABBAR_ITEMS.map((item, index) => ({
...item,
active: index === 0,
}))
}
}));
};
return {
tabbarList,
@@ -107,11 +107,11 @@ function createTabbarState() {
setTabbarItem,
setTabbarItemActive,
resetTabbar,
}
};
}
/** 全局单例实例 */
let tabbarInstance: ReturnType<typeof createTabbarState> | null = null
let tabbarInstance: ReturnType<typeof createTabbarState> | null = null;
/**
* TabBar 状态管理 Hook
@@ -123,14 +123,14 @@ let tabbarInstance: ReturnType<typeof createTabbarState> | null = null
* @param options.singleton 是否使用单例模式,默认 true
*/
export function useTabbar(options?: { singleton?: boolean }) {
const { singleton = true } = options ?? {}
const { singleton = true } = options ?? {};
if (singleton) {
if (!tabbarInstance) {
tabbarInstance = createTabbarState()
tabbarInstance = createTabbarState();
}
return tabbarInstance
return tabbarInstance;
}
return createTabbarState()
return createTabbarState();
}

View File

@@ -105,10 +105,6 @@ import { checkLogin, isLoggedIn } from "@/utils/auth";
import LogAPI, { type VisitOverview as ApiVisitOverview, type VisitTrend } from "@/api/log";
import NoticeAPI, { type NoticeItem } from "@/api/notice";
// ============================================================================
// 类型定义
// ============================================================================
type VisitOverviewVO = ApiVisitOverview;
interface NavItem {
@@ -118,22 +114,17 @@ interface NavItem {
perm: string;
}
// ============================================================================
// Hooks
// ============================================================================
const router = useRouter();
const userStore = useUserStore();
// custom-navbar 组件内部已处理导航栏高度与胶囊避让
// ============================================================================
// 响应式数据
// ============================================================================
const current = ref(0);
const recentDaysRange = ref(7);
const swiperList = ref(["https://www.youlai.tech/storage/youlai/bg02.png" ,"https://www.youlai.tech/storage/blog/banner9.png" ]);
const swiperList = ref([
"https://www.youlai.tech/storage/youlai/bg02.png",
"https://www.youlai.tech/storage/blog/banner9.png",
]);
const visitOverviewData = ref<VisitOverviewVO>({
todayUvCount: 0,
@@ -228,10 +219,6 @@ const chartOpts = ref({
},
});
// ============================================================================
// 数据加载
// ============================================================================
function loadAppVersion() {
try {
const p: any = (globalThis as any).plus;
@@ -287,10 +274,6 @@ async function loadVisitTrendData() {
}
}
// ============================================================================
// 事件处理
// ============================================================================
function handleNavClick(item: NavItem) {
// 未登录 / 无权限时:展示默认导航,但点击统一跳登录
if (!isLogged.value || !hasAnyPerm.value) {
@@ -332,10 +315,6 @@ function handleDataRangeChange({ value }: { value: number }) {
loadVisitTrendData();
}
// ============================================================================
// 生命周期
// ============================================================================
onReady(() => {
loadAppVersion();
loadNoticeData();
@@ -491,13 +470,13 @@ onShow(() => {
&--uv,
&--green {
background: #34d19d;
background: var(--color-success);
box-shadow: 0 0 10rpx rgba(52, 209, 157, 0.35);
}
&--pv,
&--blue {
background: #4d80f0;
background: var(--color-primary);
box-shadow: 0 0 10rpx rgba(77, 128, 240, 0.3);
}
}
@@ -512,12 +491,12 @@ onShow(() => {
&--uv,
&--green {
color: #34d19d;
color: var(--color-success);
}
&--pv,
&--blue {
color: #4d80f0;
color: var(--color-primary);
}
}

View File

@@ -214,7 +214,9 @@
/>
<view
class="login__code-btn"
:class="bindSmsCountdown > 0 ? 'login__code-btn--disabled' : 'login__code-btn--active'"
:class="
bindSmsCountdown > 0 ? 'login__code-btn--disabled' : 'login__code-btn--active'
"
@click="handleSendBindCode"
>
{{ bindSmsCountdown > 0 ? `${bindSmsCountdown}s` : "获取验证码" }}
@@ -235,10 +237,14 @@
<!-- 协议确认弹窗 -->
<wd-message-box selector="policy-box" root-portal>
<view class="text-center text-sm text-gray-500 leading-relaxed">
<view class="policy-dialog__content">
请阅读并同意有来技术
<text class="text-blue-500" @click.stop="navigateToAgreement('user')">用户协议</text>
<text class="text-blue-500" @click.stop="navigateToAgreement('privacy')">隐私政策</text>
<text class="policy-dialog__link" @click.stop="navigateToAgreement('user')">
用户协议
</text>
<text class="policy-dialog__link" @click.stop="navigateToAgreement('privacy')">
隐私政策
</text>
</view>
</wd-message-box>
@@ -305,7 +311,11 @@ const pendingWechatPhoneCode = ref("");
// 计算属性
const loginModeDesc = computed(() => {
const modeMap = { PASSWORD: "使用账号密码登录", SMS: "使用手机验证码登录", WECHAT: "使用微信快捷登录" };
const modeMap = {
PASSWORD: "使用账号密码登录",
SMS: "使用手机验证码登录",
WECHAT: "使用微信快捷登录",
};
return modeMap[loginMode.value];
});
@@ -348,7 +358,10 @@ const startSmsCountdown = (
countdown.value -= 1;
if (countdown.value <= 0) {
countdown.value = 0;
if (timer.value) { clearInterval(timer.value); timer.value = null; }
if (timer.value) {
clearInterval(timer.value);
timer.value = null;
}
}
}, 1000);
};
@@ -394,7 +407,9 @@ const openPolicyDialog = (action: "FORM" | "WECHAT_PHONE", phoneCode = "") => {
// 表单登录
async function doFormLogin() {
if (!canSubmit.value) {
toast.error(loginMode.value === "PASSWORD" ? "请输入用户名和密码" : "请输入正确的手机号和验证码");
toast.error(
loginMode.value === "PASSWORD" ? "请输入用户名和密码" : "请输入正确的手机号和验证码"
);
return;
}
if (loading.value) return;
@@ -408,7 +423,10 @@ async function doFormLogin() {
captchaCode: formData.value.captchaCode,
});
} else {
await userStore.loginBySms({ mobile: formData.value.username.trim(), code: formData.value.code });
await userStore.loginBySms({
mobile: formData.value.username.trim(),
code: formData.value.code,
});
}
await userStore.getInfo();
toast.success("登录成功");
@@ -422,15 +440,24 @@ async function doFormLogin() {
}
const handleLogin = async () => {
if (!isAgreePolicy.value) { openPolicyDialog("FORM"); return; }
if (!isAgreePolicy.value) {
openPolicyDialog("FORM");
return;
}
await doFormLogin();
};
const handleSendCode = async () => {
if (smsCountdown.value > 0) return;
const mobile = formData.value.username.trim();
if (!mobile) { toast.error("请输入手机号"); return; }
if (!isValidMobile(mobile)) { toast.error("请输入正确的手机号"); return; }
if (!mobile) {
toast.error("请输入手机号");
return;
}
if (!isValidMobile(mobile)) {
toast.error("请输入正确的手机号");
return;
}
try {
await AuthAPI.sendSmsLoginCode(mobile);
toast.success("验证码已发送");
@@ -443,13 +470,22 @@ const handleSendCode = async () => {
// 微信登录
const handleWechatPhoneLogin = async (e: any) => {
const phoneCode = e.detail.code;
if (!isAgreePolicy.value) { openPolicyDialog("WECHAT_PHONE", phoneCode); return; }
if (!phoneCode) { await handleWechatSilentLogin(); return; }
if (!isAgreePolicy.value) {
openPolicyDialog("WECHAT_PHONE", phoneCode);
return;
}
if (!phoneCode) {
await handleWechatSilentLogin();
return;
}
await doWechatPhoneLogin(phoneCode);
};
async function doWechatPhoneLogin(phoneCode: string) {
if (!phoneCode) { await handleWechatSilentLogin(); return; }
if (!phoneCode) {
await handleWechatSilentLogin();
return;
}
loading.value = true;
try {
const { code: loginCode } = await uni.login();
@@ -489,7 +525,10 @@ const handleWechatSilentLogin = async () => {
const handleSendBindCode = async () => {
if (bindSmsCountdown.value > 0) return;
const mobile = bindMobileForm.value.mobile.trim();
if (!isValidMobile(mobile)) { toast.error("请输入正确的手机号"); return; }
if (!isValidMobile(mobile)) {
toast.error("请输入正确的手机号");
return;
}
try {
await AuthAPI.sendSmsLoginCode(mobile);
toast.success("验证码已发送");
@@ -502,14 +541,23 @@ const handleSendBindCode = async () => {
const resetBindForm = () => {
bindMobileForm.value = { mobile: "", code: "" };
bindSmsCountdown.value = 0;
if (bindSmsTimer.value) { clearInterval(bindSmsTimer.value); bindSmsTimer.value = null; }
if (bindSmsTimer.value) {
clearInterval(bindSmsTimer.value);
bindSmsTimer.value = null;
}
};
const handleBindMobile = async () => {
if (bindLoading.value) return;
const { mobile, code } = bindMobileForm.value;
if (!isValidMobile(mobile)) { toast.error("请输入正确的手机号"); return; }
if (!code.trim()) { toast.error("请输入验证码"); return; }
if (!isValidMobile(mobile)) {
toast.error("请输入正确的手机号");
return;
}
if (!code.trim()) {
toast.error("请输入验证码");
return;
}
bindLoading.value = true;
try {
await userStore.bindMobileForWxMa({ openid: wechatOpenid.value, mobile, smsCode: code });
@@ -526,12 +574,16 @@ const handleBindMobile = async () => {
};
const navigateToAgreement = (type: string) => {
const url = type === "user" ? "/pages/mine/settings/agreement/index" : "/pages/mine/settings/privacy/index";
const url =
type === "user" ? "/pages/mine/settings/agreement/index" : "/pages/mine/settings/privacy/index";
uni.navigateTo({ url });
};
const handleBack = () => {
if (getCurrentPages().length > 1) { uni.navigateBack(); return; }
if (getCurrentPages().length > 1) {
uni.navigateBack();
return;
}
uni.reLaunch({ url: "/pages/index/index" });
};
@@ -562,9 +614,6 @@ onUnload(() => {
</script>
<style lang="scss" scoped>
// ==========================================================================
// 页面背景
// ==========================================================================
.login {
background: linear-gradient(
135deg,
@@ -574,9 +623,6 @@ onUnload(() => {
);
}
// ==========================================================================
// 背景装饰(模糊光晕圆)
// ==========================================================================
.login__decoration {
position: fixed;
top: 0;
@@ -597,7 +643,7 @@ onUnload(() => {
left: -160rpx;
width: 480rpx;
height: 480rpx;
background-color: rgba(96, 165, 250, 0.2);
background-color: var(--color-primary-alpha-20);
}
&--2 {
@@ -605,13 +651,10 @@ onUnload(() => {
bottom: -160rpx;
width: 640rpx;
height: 640rpx;
background-color: rgba(59, 130, 246, 0.15);
background-color: var(--color-primary-alpha-15);
}
}
// ==========================================================================
// 导航栏
// ==========================================================================
.login__navbar {
position: fixed;
top: 0;
@@ -642,8 +685,8 @@ onUnload(() => {
.login__navbar-btn {
left: 0;
background-color: rgba(255, 255, 255, 0.18);
border: 2rpx solid rgba(255, 255, 255, 0.35);
background-color: var(--color-glass);
border: 2rpx solid var(--color-border-glass);
border-radius: 999rpx;
&--active {
@@ -660,19 +703,16 @@ onUnload(() => {
font-size: 44rpx;
font-weight: 500;
line-height: 1;
color: rgba(15, 23, 42, 0.92);
color: var(--color-text);
}
.login__navbar-title {
font-size: 32rpx;
font-weight: 600;
letter-spacing: 0.08em;
color: rgba(15, 23, 42, 0.92);
color: var(--color-text);
}
// ==========================================================================
// 主内容区 —— 普通流式布局,不需要高 z-index
// ==========================================================================
.login__body {
display: flex;
flex-direction: column;
@@ -680,9 +720,6 @@ onUnload(() => {
padding: 0 96rpx;
}
// ==========================================================================
// 品牌 Logo
// ==========================================================================
.login__brand {
display: flex;
flex-direction: column;
@@ -704,13 +741,10 @@ onUnload(() => {
color: var(--color-text);
}
// ==========================================================================
// 登录卡片
// ==========================================================================
.login__card {
width: 100%;
padding: 64rpx;
background-color: rgba(255, 255, 255, 0.95);
background-color: var(--color-bg-alpha-95);
backdrop-filter: blur(24px);
border-radius: 48rpx;
box-shadow: 0 20rpx 50rpx -10rpx rgba(0, 0, 0, 0.1);
@@ -735,9 +769,6 @@ onUnload(() => {
color: var(--color-text-secondary);
}
// ==========================================================================
// 表单区域
// ==========================================================================
.login__form-item {
margin-top: 32rpx;
@@ -771,9 +802,6 @@ onUnload(() => {
border-radius: 12rpx;
}
// ==========================================================================
// 微信登录按钮
// ==========================================================================
.login__wx-btn {
display: flex;
align-items: center;
@@ -798,9 +826,6 @@ onUnload(() => {
margin-right: 16rpx;
}
// ==========================================================================
// 验证码按钮
// ==========================================================================
.login__code-btn {
display: flex;
align-items: center;
@@ -821,9 +846,6 @@ onUnload(() => {
}
}
// ==========================================================================
// 登录方式切换
// ==========================================================================
.login__mode-switch {
display: flex;
justify-content: center;
@@ -843,9 +865,6 @@ onUnload(() => {
border-bottom: 2rpx solid var(--color-primary);
}
// ==========================================================================
// 分割线
// ==========================================================================
.login__divider {
display: flex;
align-items: center;
@@ -864,9 +883,6 @@ onUnload(() => {
color: var(--color-text-placeholder);
}
// ==========================================================================
// 第三方登录入口
// ==========================================================================
.login__oauth-row {
display: flex;
justify-content: center;
@@ -882,9 +898,6 @@ onUnload(() => {
}
}
// ==========================================================================
// 协议勾选
// ==========================================================================
.login__policy {
display: flex;
align-items: flex-start;
@@ -904,9 +917,17 @@ onUnload(() => {
color: var(--color-primary);
}
// ==========================================================================
// 绑定手机号弹窗
// ==========================================================================
.policy-dialog__content {
font-size: 26rpx;
line-height: 1.625;
color: var(--color-text-secondary);
text-align: center;
}
.policy-dialog__link {
color: var(--color-primary);
}
.login__bind-panel {
padding: 48rpx;
}
@@ -920,9 +941,6 @@ onUnload(() => {
color: var(--color-text);
}
// ==========================================================================
// 演示环境提示
// ==========================================================================
.login__demo-hint {
display: flex;
justify-content: center;

View File

@@ -334,9 +334,6 @@ onMounted(() => {
}
}
// ==========================================================================
// 头部区域Logo + 应用信息)
// ==========================================================================
.about__header {
display: flex;
align-items: center;

View File

@@ -1,5 +1,5 @@
<template>
<view class="page account-page">
<view class="page py-16rpx">
<wd-card>
<wd-cell-group border>
<wd-cell
@@ -73,7 +73,7 @@
:rules="rules.confirmPassword"
/>
</wd-cell-group>
<view class="account-page__submit">
<view class="p-24rpx">
<wd-button type="primary" size="large" block @click="handleSubmit">提交</wd-button>
</view>
</wd-form>
@@ -113,7 +113,7 @@
</template>
</wd-input>
</wd-cell-group>
<view class="account-page__submit">
<view class="p-24rpx">
<wd-button type="primary" size="large" block @click="handleSubmit">提交</wd-button>
</view>
</wd-form>
@@ -153,7 +153,7 @@
</template>
</wd-input>
</wd-cell-group>
<view class="account-page__submit">
<view class="p-24rpx">
<wd-button type="primary" size="large" block @click="handleSubmit">提交</wd-button>
</view>
</wd-form>
@@ -359,12 +359,3 @@ onUnmounted(() => {
}
});
</script>
<style lang="scss" scoped>
.account-page {
padding: 16rpx 0;
}
.account-page__submit {
padding: 24rpx;
}
</style>

View File

@@ -84,7 +84,6 @@
mode="aspectFill"
/>
<view class="community-card__mask" />
<view class="community-card__sparkle" />
<view class="community-card__logo">
<image src="/static/logo.png" mode="aspectFit" class="w-full h-full" />
</view>
@@ -260,8 +259,8 @@ const genderIconName = computed(() => {
});
const genderIconClass = computed(() => {
if (normalizedGender.value === 1) return "gender-icon--male";
if (normalizedGender.value === 2) return "gender-icon--female";
if (normalizedGender.value === 1) return "profile-card__gender--male";
if (normalizedGender.value === 2) return "profile-card__gender--female";
return "";
});
@@ -492,26 +491,14 @@ const openOfficialAccount = () => {
position: relative;
padding: 28rpx 28rpx;
overflow: hidden;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.22) 0%, rgba(255, 255, 255, 0.12) 100%);
background: linear-gradient(135deg, var(--color-glass) 0%, var(--color-glass-light) 100%);
backdrop-filter: blur(20px);
border: 1rpx solid rgba(255, 255, 255, 0.35);
border: 1rpx solid var(--color-border-glass);
border-radius: 28rpx;
box-shadow:
0 8rpx 32rpx rgba(0, 0, 0, 0.08),
0 2rpx 8rpx rgba(0, 0, 0, 0.04),
inset 0 1rpx 0 rgba(255, 255, 255, 0.25);
}
.profile-card::before {
position: absolute;
top: -50%;
right: -20%;
width: 200rpx;
height: 200rpx;
pointer-events: none;
content: "";
background: radial-gradient(circle, rgba(255, 255, 255, 0.25) 0%, transparent 70%);
border-radius: 50%;
inset 0 1rpx 0 var(--color-glass);
}
.profile-card__header {
@@ -530,7 +517,7 @@ const openOfficialAccount = () => {
flex-shrink: 0;
width: 120rpx;
height: 120rpx;
border: 3rpx solid rgba(255, 255, 255, 0.9);
border: 3rpx solid var(--color-border-glass-strong);
border-radius: 50%;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
}
@@ -553,7 +540,7 @@ const openOfficialAccount = () => {
width: 18rpx;
height: 18rpx;
background: var(--color-success);
border: 3rpx solid rgba(255, 255, 255, 0.95);
border: 3rpx solid var(--color-border-glass-strong);
border-radius: 50%;
box-shadow: 0 4rpx 12rpx rgba(15, 23, 42, 0.12);
}
@@ -574,9 +561,9 @@ const openOfficialAccount = () => {
justify-content: center;
width: 52rpx;
height: 52rpx;
background: rgba(255, 255, 255, 0.14);
background: var(--color-glass-light);
backdrop-filter: blur(10px);
border: 1rpx solid rgba(255, 255, 255, 0.22);
border: 1rpx solid var(--color-border-glass);
border-radius: 999rpx;
}
@@ -593,15 +580,15 @@ const openOfficialAccount = () => {
font-size: 18rpx;
color: var(--color-text-inverse);
background: var(--color-danger);
border: 2rpx solid rgba(255, 255, 255, 0.95);
border: 2rpx solid var(--color-border-glass-strong);
border-radius: 999rpx;
}
.gender-icon--male {
.profile-card__gender--male {
background: var(--color-primary);
}
.gender-icon--female {
.profile-card__gender--female {
background: var(--color-danger);
}
@@ -621,8 +608,7 @@ const openOfficialAccount = () => {
min-width: 0;
font-size: 38rpx;
font-weight: 700;
color: rgba(255, 255, 255, 0.98);
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
color: var(--color-text-inverse);
}
.profile-card__desc {
@@ -631,7 +617,7 @@ const openOfficialAccount = () => {
overflow: hidden;
font-size: 24rpx;
line-height: 1.6;
color: rgba(255, 255, 255, 0.84);
color: var(--color-text-inverse);
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
@@ -641,7 +627,7 @@ const openOfficialAccount = () => {
display: block;
margin-top: 14rpx;
font-size: 24rpx;
color: rgba(255, 255, 255, 0.84);
color: var(--color-text-inverse);
}
.profile-card__tags {
@@ -658,9 +644,9 @@ const openOfficialAccount = () => {
gap: 8rpx;
padding: 8rpx 14rpx;
font-size: 22rpx;
color: rgba(255, 255, 255, 0.9);
background: rgba(255, 255, 255, 0.15);
border: 1rpx solid rgba(255, 255, 255, 0.2);
color: var(--color-text-inverse);
background: var(--color-glass-light);
border: 1rpx solid var(--color-border-glass);
border-radius: 12rpx;
}
@@ -708,27 +694,12 @@ const openOfficialAccount = () => {
z-index: var(--z-sticky);
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0.62) 0%,
rgba(255, 255, 255, 0.34) 55%,
rgba(255, 255, 255, 0.58) 100%
var(--color-glass) 0%,
var(--color-glass-light) 55%,
var(--color-glass) 100%
);
}
.community-card__sparkle {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: var(--z-sticky);
pointer-events: none;
background:
radial-gradient(circle at 18% 30%, rgba(255, 255, 255, 0.42) 0%, rgba(255, 255, 255, 0) 52%),
radial-gradient(circle at 72% 20%, rgba(255, 255, 255, 0.26) 0%, rgba(255, 255, 255, 0) 58%),
radial-gradient(circle at 60% 78%, rgba(59, 130, 246, 0.12) 0%, rgba(59, 130, 246, 0) 60%);
opacity: 0.55;
}
.community-card__logo {
position: relative;
z-index: var(--z-sticky);
@@ -736,7 +707,7 @@ const openOfficialAccount = () => {
width: 84rpx;
height: 84rpx;
padding: 10rpx;
background: rgba(255, 255, 255, 0.96);
background: var(--color-bg-alpha-95);
border: 1rpx solid var(--color-border-glass);
border-radius: 20rpx;
box-shadow: 0 6rpx 18rpx rgba(15, 23, 42, 0.06);
@@ -757,7 +728,7 @@ const openOfficialAccount = () => {
justify-content: center;
width: 44rpx;
height: 44rpx;
background: rgba(255, 255, 255, 0.68);
background: var(--color-glass);
border-radius: 999rpx;
}
@@ -776,7 +747,7 @@ const openOfficialAccount = () => {
letter-spacing: 0.4rpx;
}
:deep(.community-card__tag) {
.community-card__tag {
margin-top: 14rpx;
}
@@ -963,7 +934,7 @@ const openOfficialAccount = () => {
padding: 36rpx 32rpx 0;
}
:deep(.profile-card__button) {
.profile-card__button {
flex-shrink: 0;
min-width: 156rpx;
height: 72rpx;
@@ -974,7 +945,7 @@ const openOfficialAccount = () => {
border-radius: 999rpx;
}
:deep(.logout-btn) {
.logout-btn {
width: 100%;
height: 88rpx;
font-size: 28rpx;
@@ -985,7 +956,7 @@ const openOfficialAccount = () => {
box-shadow: 0 12rpx 30rpx var(--color-danger-shadow);
}
:deep(.loading-center) {
.loading-center {
display: flex;
flex-direction: column;
align-items: center;

View File

@@ -13,11 +13,7 @@
</view>
<view class="qrcode-card">
<image
src="/static/images/qrcode-official.png"
class="qrcode-card__img"
mode="aspectFit"
/>
<image src="/static/images/qrcode-official.png" class="qrcode-card__img" mode="aspectFit" />
</view>
<view class="qrcode-hint">长按识别二维码关注</view>

View File

@@ -35,8 +35,11 @@
<script lang="ts" setup>
import { useUserStore } from "@/store";
import { onLoad } from "@dcloudio/uni-app";
import { useToast, useMessage } from "wot-design-uni";
const userStore = useUserStore();
const toast = useToast();
const { messageBox } = useMessage();
const isLogin = computed(() => !!userStore.userInfo);
// 主题设置
@@ -179,8 +182,7 @@ onLoad(() => {
margin-top: 60rpx;
}
// 退出登录按钮样式 - 使用更具体的选择器替代 !important
:deep(.wd-button.logout-btn) {
:deep(.logout-btn) {
width: 80%;
height: 80rpx;
font-size: 32rpx;
@@ -193,7 +195,7 @@ onLoad(() => {
}
}
:deep(.loading-center) {
.loading-center {
display: flex;
flex-direction: column;
align-items: center;
@@ -202,11 +204,11 @@ onLoad(() => {
border-radius: 12rpx;
}
:deep(.loading-center .wd-loading__spinner) {
.loading-center :deep(.wd-loading__spinner) {
margin: 0 auto;
}
:deep(.loading-center .wd-loading__text) {
.loading-center :deep(.wd-loading__text) {
margin-top: 20rpx;
color: var(--color-text-inverse);
text-align: center;

View File

@@ -71,11 +71,7 @@
</view>
<!-- 自定义颜色输入弹窗 -->
<wd-popup
v-model="showCustomColorInput"
position="bottom"
custom-class="popup-bottom"
>
<wd-popup v-model="showCustomColorInput" position="bottom" custom-class="popup-bottom">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">自定义主题色</text>

View File

@@ -19,9 +19,9 @@
>
<!-- 自定义节点内容ID + 名称 + 状态 -->
<template #content="{ node }">
<view class="flex-1 flex items-center gap-16rpx">
<text class="w-120rpx text-24rpx color-text-secondary">{{ node.id }}</text>
<text class="flex-1 truncate">{{ node.name }}</text>
<view class="dept-node">
<text class="dept-node__id">{{ node.id }}</text>
<text class="dept-node__name">{{ node.name }}</text>
<wd-tag :type="node.status === 1 ? 'success' : 'danger'" size="small">
{{ node.status === 1 ? "正常" : "禁用" }}
</wd-tag>
@@ -72,10 +72,7 @@
</wd-popup>
<!-- 浮动新增按钮 -->
<wd-fab
v-if="hasPermission('sys:dept:create') && !dialog.visible"
@click="openDeptDialog()"
/>
<wd-fab v-if="hasPermission('sys:dept:create') && !dialog.visible" @click="openDeptDialog()" />
<!-- 操作菜单 -->
<wd-action-sheet
@@ -342,3 +339,25 @@ export default { options: { styleIsolation: "shared" } };
}
}
</route>
<style lang="scss" scoped>
.dept-node {
display: flex;
flex: 1;
align-items: center;
gap: 16rpx;
}
.dept-node__id {
width: 120rpx;
font-size: 24rpx;
color: var(--color-text-secondary);
}
.dept-node__name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

View File

@@ -84,10 +84,7 @@
@select="handleActionSelect"
/>
<wd-fab
v-if="hasPermission('sys:dict:create') && !dialog.visible"
@click="openDictDialog()"
/>
<wd-fab v-if="hasPermission('sys:dict:create') && !dialog.visible" @click="openDictDialog()" />
</view>
</template>
@@ -96,16 +93,13 @@ import { onLoad, onReachBottom } from "@dcloudio/uni-app";
import { useRouter } from "uni-mini-router";
import { LoadMoreState } from "wot-design-uni/components/wd-loadmore/types";
import { FormRules } from "wot-design-uni/components/wd-form/types";
import { useToast } from "wot-design-uni";
import DictAPI, {
type DictTypeForm,
type DictTypePageQuery,
type DictTypeItem,
} from "@/api/dict";
import { useToast, useMessage } from "wot-design-uni";
import DictAPI, { type DictTypeForm, type DictTypePageQuery, type DictTypeItem } from "@/api/dict";
import { hasPermission } from "@/utils/permission";
const router = useRouter();
const toast = useToast();
const { messageBox } = useMessage();
const loadMoreState = ref<LoadMoreState>("loading");
const formRef = ref();
const submitting = ref(false);
@@ -216,13 +210,19 @@ function showDictActions(item: DictTypeItem) {
actions.push({ name: "删除", color: "var(--color-danger)" });
actionMap["删除"] = async () => {
try {
await messageBox({ title: "确认删除", msg: `确定要删除字典「${item.name}」吗?`, type: "warning" });
await messageBox({
title: "确认删除",
msg: `确定要删除字典「${item.name}」吗?`,
type: "warning",
});
if (item.id) {
await DictAPI.deleteByIds(String(item.id));
toast.success("删除成功");
loadDictTypeList();
}
} catch {}
} catch {
// 用户取消操作
}
};
}

View File

@@ -100,15 +100,12 @@
import { onLoad, onReachBottom } from "@dcloudio/uni-app";
import { LoadMoreState } from "wot-design-uni/components/wd-loadmore/types";
import { FormRules } from "wot-design-uni/components/wd-form/types";
import { useToast } from "wot-design-uni";
import DictAPI, {
type DictItemForm,
type DictItemPageQuery,
type DictDataItem,
} from "@/api/dict";
import { useToast, useMessage } from "wot-design-uni";
import DictAPI, { type DictItemForm, type DictItemPageQuery, type DictDataItem } from "@/api/dict";
import { hasPermission } from "@/utils/permission";
const toast = useToast();
const { messageBox } = useMessage();
const dictCode = ref<string>("");
const pageTitle = ref<string>("字典数据");
@@ -231,13 +228,19 @@ function showItemActions(item: DictDataItem) {
actions.push({ name: "删除", color: "var(--color-danger)" });
actionMap["删除"] = async () => {
try {
await messageBox({ title: "确认删除", msg: `确定要删除字典数据「${item.label}」吗?`, type: "warning" });
await messageBox({
title: "确认删除",
msg: `确定要删除字典数据「${item.label}」吗?`,
type: "warning",
});
if (item.id) {
await DictAPI.deleteItems(dictCode.value, String(item.id));
toast.success("删除成功");
loadItemList();
}
} catch {}
} catch {
// 用户取消操作
}
};
}

View File

@@ -1,9 +1,6 @@
<template>
<custom-navbar title="工作台" :show-back="false" />
<view
class="page page--tabbar"
:style="{ padding: `${navbar.totalHeight.value + 8}px 32rpx 0` }"
>
<view class="page page--tabbar" :style="{ padding: `${navbar.totalHeight.value + 8}px 32rpx 0` }">
<template v-for="(item, index) in visibleGridList" :key="index">
<wd-card :title="item.title">
<wd-grid clickable :column="4">
@@ -13,10 +10,10 @@
use-slot
@itemclick="handleNavClick(child)"
>
<view class="p-2">
<view class="work-grid__icon p-2">
<image class="w-72rpx h-72rpx rounded-8rpx" :src="child.icon" />
</view>
<view class="text">{{ child.title }}</view>
<view class="work-grid__label">{{ child.title }}</view>
</wd-grid-item>
</wd-grid>
</wd-card>
@@ -28,19 +25,14 @@
import { computed } from "vue";
import { useRouter } from "uni-mini-router";
import { useNavbar } from "@/composables/useNavbar";
import { useUserStore } from "@/store";
import { menuConfig } from "@/config/menu";
import { checkLogin, isLoggedIn } from "@/utils/auth";
import { hasPermission as checkPermission } from "@/utils/permission";
const router = useRouter();
const userStore = useUserStore();
const navbar = useNavbar({ hasTabbar: true });
// 用户权限列表
const userPerms = computed(() => userStore.userInfo?.perms || []);
// 是否已登录
const isLogged = computed(() => isLoggedIn());

View File

@@ -107,10 +107,7 @@
</wd-popup>
<!-- 浮动新增按钮 -->
<wd-fab
v-if="hasPermission('sys:menu:create') && !dialog.visible"
@click="openMenuDialog()"
/>
<wd-fab v-if="hasPermission('sys:menu:create') && !dialog.visible" @click="openMenuDialog()" />
<!-- 操作菜单 -->
<wd-action-sheet

View File

@@ -19,11 +19,11 @@
>
<!-- 主信息行 -->
<view class="flex-start">
<view class="flex-1">
<view class="notice-card__main">
<view class="flex-start mt-12rpx">
<text class="font-bold text-32rpx">{{ item.title }}</text>
<text class="notice-card__title">{{ item.title }}</text>
</view>
<text class="text-24rpx color-text-secondary">
<text class="notice-card__publisher">
{{ item.publisherName || "系统管理员" }}
</text>
</view>
@@ -33,27 +33,27 @@
</view>
<!-- 辅助信息行 -->
<view class="flex gap-24rpx mt-12rpx">
<view class="flex-start min-w-0">
<view class="notice-card__meta">
<view class="notice-card__detail">
<wd-icon name="user" size="16" class="color-text-secondary" />
<text class="ml-8rpx text-24rpx color-text-secondary">
<text class="notice-card__detail-text">
{{ item.targetType === 1 ? "全体" : "指定用户" }}
</text>
</view>
<view class="flex-start min-w-0">
<view class="notice-card__detail">
<wd-icon name="warning" size="16" class="color-text-secondary" />
<text class="ml-8rpx text-24rpx color-text-secondary">
<text class="notice-card__detail-text">
{{ getLevelText(item.level) }}
</text>
</view>
</view>
<!-- 元信息行 -->
<view class="flex-between mt-16rpx">
<text class="text-24rpx color-text-placeholder">{{ formatTime(item) }}</text>
<view class="notice-card__footer">
<text class="notice-card__time">{{ formatTime(item) }}</text>
<view
class="w-64rpx h-64rpx flex-center rounded-full"
hover-class="bg-[var(--color-text-placeholder)]/16"
class="notice-card__action"
hover-class="notice-card__action--hover"
@click.stop="showNoticeActions(item)"
>
<wd-icon name="more" size="16" class="color-text-secondary" />
@@ -156,7 +156,7 @@
import { onLoad, onReachBottom } from "@dcloudio/uni-app";
import { LoadMoreState } from "wot-design-uni/components/wd-loadmore/types";
import { FormRules } from "wot-design-uni/components/wd-form/types";
import { useToast } from "wot-design-uni";
import { useToast, useMessage } from "wot-design-uni";
import NoticeAPI, {
type NoticePageQuery,
NoticeItem,
@@ -166,6 +166,7 @@ import NoticeAPI, {
import { hasPermission } from "@/utils/permission";
const toast = useToast();
const { messageBox } = useMessage();
const loadMoreState = ref<LoadMoreState>("loading");
const formRef = ref();
const submitting = ref(false);
@@ -324,22 +325,34 @@ function showNoticeActions(item: NoticeItem) {
actions.push({ name: "删除", color: "var(--color-danger)" });
actionMap["删除"] = async () => {
try {
await messageBox({ title: "确认删除", msg: `确定要删除通知「${item.title}」吗?`, type: "warning" });
await messageBox({
title: "确认删除",
msg: `确定要删除通知「${item.title}」吗?`,
type: "warning",
});
await NoticeAPI.deleteByIds(item.id);
toast.success("删除成功");
loadNoticeList();
} catch {}
} catch {
// 用户取消操作
}
};
}
if (hasPermission("sys:notice:publish")) {
actions.push({ name: "发布" });
actionMap["发布"] = async () => {
try {
await messageBox({ title: "确认发布", msg: `确定要发布通知「${item.title}」吗?`, type: "warning" });
await messageBox({
title: "确认发布",
msg: `确定要发布通知「${item.title}」吗?`,
type: "warning",
});
await NoticeAPI.publish(Number(item.id));
toast.success("发布成功");
loadNoticeList();
} catch {}
} catch {
// 用户取消操作
}
};
}
} else {
@@ -347,11 +360,17 @@ function showNoticeActions(item: NoticeItem) {
actions.push({ name: "撤回", color: "var(--color-warning)" });
actionMap["撤回"] = async () => {
try {
await messageBox({ title: "确认撤回", msg: `确定要撤回通知「${item.title}」吗?`, type: "warning" });
await messageBox({
title: "确认撤回",
msg: `确定要撤回通知「${item.title}」吗?`,
type: "warning",
});
await NoticeAPI.revoke(Number(item.id));
toast.success("撤回成功");
loadNoticeList();
} catch {}
} catch {
// 用户取消操作
}
};
}
}
@@ -390,3 +409,62 @@ export default { options: { styleIsolation: "shared" } };
}
}
</route>
<style lang="scss" scoped>
.notice-card__main {
flex: 1;
}
.notice-card__title {
font-weight: 700;
font-size: 32rpx;
}
.notice-card__publisher {
font-size: 24rpx;
color: var(--color-text-secondary);
}
.notice-card__meta {
display: flex;
gap: 24rpx;
margin-top: 12rpx;
}
.notice-card__detail {
display: flex;
align-items: flex-start;
min-width: 0;
}
.notice-card__detail-text {
margin-left: 8rpx;
font-size: 24rpx;
color: var(--color-text-secondary);
}
.notice-card__footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16rpx;
}
.notice-card__time {
font-size: 24rpx;
color: var(--color-text-placeholder);
}
.notice-card__action {
display: flex;
align-items: center;
justify-content: center;
width: 64rpx;
height: 64rpx;
border-radius: 50%;
}
.notice-card__action--hover {
background: rgba(var(--color-text-placeholder-rgb, 148, 163, 184), 0.16);
}
</style>

View File

@@ -279,7 +279,7 @@ export default { options: { styleIsolation: "shared" } };
box-shadow: 0 -2rpx 16rpx rgba(0, 0, 0, 0.05);
}
:deep(.bottom-bar .wd-button) {
.bottom-bar :deep(.wd-button) {
flex: 1;
}
</style>

View File

@@ -19,11 +19,11 @@
>
<!-- 主信息行 -->
<view class="flex-start">
<view class="flex-1">
<view class="role-card__main">
<view class="flex-start mt-12rpx">
<text class="font-bold text-32rpx">{{ item.name }}</text>
<text class="role-card__name">{{ item.name }}</text>
</view>
<text class="text-24rpx color-text-secondary">{{ item.code }}</text>
<text class="role-card__code">{{ item.code }}</text>
</view>
<wd-tag :type="item.status === 1 ? 'success' : 'danger'" plain>
{{ item.status === 1 ? "正常" : "禁用" }}
@@ -31,23 +31,23 @@
</view>
<!-- 辅助信息行 -->
<view class="flex gap-24rpx mt-12rpx">
<view class="flex-start min-w-0">
<view class="role-card__meta">
<view class="role-card__detail">
<wd-icon name="view" size="16" class="color-text-secondary" />
<text class="ml-8rpx text-24rpx color-text-secondary">{{ item.dataScopeLabel }}</text>
<text class="role-card__detail-text">{{ item.dataScopeLabel }}</text>
</view>
<view class="flex-start min-w-0">
<view class="role-card__detail">
<wd-icon name="sort" size="16" class="color-text-secondary" />
<text class="ml-8rpx text-24rpx color-text-secondary">排序: {{ item.sort }}</text>
<text class="role-card__detail-text">排序: {{ item.sort }}</text>
</view>
</view>
<!-- 元信息行 -->
<view class="flex-between mt-16rpx">
<text class="text-24rpx color-text-placeholder">{{ item.createTime }}</text>
<view class="role-card__footer">
<text class="role-card__time">{{ item.createTime }}</text>
<view
class="w-88rpx h-88rpx flex-center rounded-full"
hover-class="bg-[var(--color-text-placeholder)]/16"
class="role-card__action"
hover-class="role-card__action--hover"
@click.stop="showRoleActions(item)"
>
<wd-icon name="more" size="18" class="color-text-secondary" />
@@ -96,10 +96,7 @@
</wd-popup>
<!-- 浮动新增按钮 -->
<wd-fab
v-if="hasPermission('sys:role:create') && !dialog.visible"
@click="openRoleDialog()"
/>
<wd-fab v-if="hasPermission('sys:role:create') && !dialog.visible" @click="openRoleDialog()" />
<!-- 操作菜单 -->
<wd-action-sheet
@@ -300,3 +297,62 @@ export default { options: { styleIsolation: "shared" } };
}
}
</route>
<style lang="scss" scoped>
.role-card__main {
flex: 1;
}
.role-card__name {
font-weight: 700;
font-size: 32rpx;
}
.role-card__code {
font-size: 24rpx;
color: var(--color-text-secondary);
}
.role-card__meta {
display: flex;
gap: 24rpx;
margin-top: 12rpx;
}
.role-card__detail {
display: flex;
align-items: flex-start;
min-width: 0;
}
.role-card__detail-text {
margin-left: 8rpx;
font-size: 24rpx;
color: var(--color-text-secondary);
}
.role-card__footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16rpx;
}
.role-card__time {
font-size: 24rpx;
color: var(--color-text-placeholder);
}
.role-card__action {
display: flex;
align-items: center;
justify-content: center;
width: 88rpx;
height: 88rpx;
border-radius: 50%;
}
.role-card__action--hover {
background: rgba(var(--color-text-placeholder-rgb, 148, 163, 184), 0.16);
}
</style>

View File

@@ -52,10 +52,10 @@
>
<!-- 主信息行 -->
<view class="flex-start">
<image class="w-80rpx h-80rpx rounded-full" :src="item.avatar" mode="aspectFill" />
<view class="flex-1 ml-16rpx">
<image class="user-card__avatar" :src="item.avatar" mode="aspectFill" />
<view class="user-card__main">
<view class="flex-start mt-12rpx">
<text class="font-bold text-32rpx">{{ item.nickname }}</text>
<text class="user-card__name">{{ item.nickname }}</text>
<wd-icon
v-if="item.gender === 1"
name="gender-male"
@@ -69,9 +69,7 @@
class="ml-8rpx"
/>
</view>
<text class="text-24rpx color-text-secondary">
{{ item.roleNames }} · {{ item.deptName }}
</text>
<text class="user-card__role">{{ item.roleNames }} · {{ item.deptName }}</text>
</view>
<wd-tag :type="item.status === 1 ? 'success' : 'danger'" plain>
{{ item.status === 1 ? "正常" : "禁用" }}
@@ -79,23 +77,23 @@
</view>
<!-- 辅助信息行 -->
<view class="flex gap-24rpx mt-12rpx">
<view v-if="item.mobile" class="flex-start min-w-0">
<view class="user-card__meta">
<view v-if="item.mobile" class="user-card__contact">
<wd-icon name="mobile" size="16" class="color-text-secondary" />
<text class="ml-8rpx text-24rpx color-text-secondary truncate">{{ item.mobile }}</text>
<text class="user-card__contact-text">{{ item.mobile }}</text>
</view>
<view v-if="item.email" class="flex-start min-w-0">
<view v-if="item.email" class="user-card__contact">
<wd-icon name="mail" size="16" class="color-text-secondary" />
<text class="ml-8rpx text-24rpx color-text-secondary truncate">{{ item.email }}</text>
<text class="user-card__contact-text">{{ item.email }}</text>
</view>
</view>
<!-- 元信息行 -->
<view class="flex-between mt-16rpx">
<text class="text-24rpx color-text-placeholder">{{ item.createTime }}</text>
<view class="user-card__footer">
<text class="user-card__time">{{ item.createTime }}</text>
<view
class="w-88rpx h-88rpx flex-center rounded-full"
hover-class="bg-[var(--color-text-placeholder)]/16"
class="user-card__action"
hover-class="user-card__action--hover"
@click.stop="showUserActions(item)"
>
<wd-icon name="more" size="18" class="color-text-secondary" />
@@ -157,10 +155,7 @@
</wd-popup>
<!-- 浮动新增按钮 -->
<wd-fab
v-if="hasPermission('sys:user:create') && !dialog.visible"
@click="openUserDialog()"
/>
<wd-fab v-if="hasPermission('sys:user:create') && !dialog.visible" @click="openUserDialog()" />
<!-- 操作菜单 -->
<wd-action-sheet
@@ -181,13 +176,22 @@
label="新密码"
placeholder="请输入新密码至少6位"
prop="password"
:rules="[{ required: true, message: '请输入新密码' }, { pattern: /^.{6,}$/, message: '密码至少需要6位字符' }]"
:rules="[
{ required: true, message: '请输入新密码' },
{ pattern: /^.{6,}$/, message: '密码至少需要6位字符' },
]"
/>
</wd-cell-group>
</wd-form>
<view class="popup-actions">
<wd-button type="info" plain @click="resetPwdDialog.visible = false">取消</wd-button>
<wd-button type="primary" :loading="resetPwdDialog.submitting" @click="handleResetPassword">确认</wd-button>
<wd-button
type="primary"
:loading="resetPwdDialog.submitting"
@click="handleResetPassword"
>
确认
</wd-button>
</view>
</view>
</wd-popup>
@@ -198,13 +202,14 @@
import { onLoad, onReachBottom } from "@dcloudio/uni-app";
import { LoadMoreState } from "wot-design-uni/components/wd-loadmore/types";
import { FormRules } from "wot-design-uni/components/wd-form/types";
import { useQueue, useToast } from "wot-design-uni";
import { useQueue, useToast, useMessage } from "wot-design-uni";
import UserAPI, { type UserPageQuery, UserItem, UserForm } from "@/api/user";
import RoleAPI from "@/api/role";
import DeptAPI from "@/api/dept";
import { hasPermission } from "@/utils/permission";
const toast = useToast();
const { messageBox } = useMessage();
const { closeOutside } = useQueue();
const loadMoreState = ref<LoadMoreState>("loading");
const formRef = ref();
@@ -414,7 +419,11 @@ const actionSheetVisible = ref(false);
const actionSheetActions = ref<{ name: string; color?: string }[]>([]);
const pendingAction = ref<Record<string, () => void>>({});
const resetPwdDialog = reactive({ visible: false, submitting: false, userId: undefined as number | undefined });
const resetPwdDialog = reactive({
visible: false,
submitting: false,
userId: undefined as number | undefined,
});
const resetPwdForm = reactive({ password: "" });
const resetPwdFormRef = ref();
@@ -440,11 +449,17 @@ function showUserActions(item: UserItem) {
actions.push({ name: "删除", color: "var(--color-danger)" });
actionMap["删除"] = async () => {
try {
await messageBox({ title: "确认删除", msg: `确定要删除用户「${item.nickname}」吗?`, type: "warning" });
await messageBox({
title: "确认删除",
msg: `确定要删除用户「${item.nickname}」吗?`,
type: "warning",
});
await UserAPI.deleteByIds(String(item.id));
toast.success("删除成功");
loadUserList();
} catch {}
} catch {
// 用户取消操作
}
};
}
@@ -515,3 +530,73 @@ export default { options: { styleIsolation: "shared" } };
}
}
</route>
<style lang="scss" scoped>
.user-card__avatar {
flex-shrink: 0;
width: 80rpx;
height: 80rpx;
border-radius: 50%;
}
.user-card__main {
flex: 1;
margin-left: 16rpx;
}
.user-card__name {
font-weight: 700;
font-size: 32rpx;
}
.user-card__role {
font-size: 24rpx;
color: var(--color-text-secondary);
}
.user-card__meta {
display: flex;
gap: 24rpx;
margin-top: 12rpx;
}
.user-card__contact {
display: flex;
align-items: flex-start;
min-width: 0;
}
.user-card__contact-text {
margin-left: 8rpx;
font-size: 24rpx;
color: var(--color-text-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-card__footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16rpx;
}
.user-card__time {
font-size: 24rpx;
color: var(--color-text-placeholder);
}
.user-card__action {
display: flex;
align-items: center;
justify-content: center;
width: 88rpx;
height: 88rpx;
border-radius: 50%;
}
.user-card__action--hover {
background: rgba(var(--color-text-placeholder-rgb, 148, 163, 184), 0.16);
}
</style>

View File

@@ -72,8 +72,6 @@ router.beforeEach(async (to, from, next) => {
next();
});
router.afterEach((to, from) => {
// 路由跳转日志(生产环境可通过 vite 配置自动移除)
});
router.afterEach(() => {});
export default router;

View File

@@ -1,8 +1,8 @@
import { defineStore } from "pinia"
import { Storage } from "@/utils/storage"
import { THEME_MODE_KEY, THEME_COLOR_KEY } from "@/constants"
import type { ThemeColorOption, ThemeMode } from "@/composables/types/theme"
import { themeColorOptions } from "@/composables/types/theme"
import { defineStore } from "pinia";
import { Storage } from "@/utils/storage";
import { THEME_MODE_KEY, THEME_COLOR_KEY } from "@/constants";
import type { ThemeColorOption, ThemeMode } from "@/composables/types/theme";
import { themeColorOptions } from "@/composables/types/theme";
/**
* 主题状态管理 Store
@@ -19,12 +19,12 @@ export const useThemeStore = defineStore("theme", () => {
// ==========================================================================
/** 当前主题模式 */
const theme = ref<ThemeMode>(Storage.get<ThemeMode>(THEME_MODE_KEY, "light"))
const theme = ref<ThemeMode>(Storage.get<ThemeMode>(THEME_MODE_KEY, "light"));
/** 当前主题色 */
const currentThemeColor = ref<ThemeColorOption>(
Storage.get<ThemeColorOption>(THEME_COLOR_KEY, themeColorOptions[0])
)
);
/** 主题变量(响应式对象) */
const themeVars = reactive({
@@ -39,14 +39,14 @@ export const useThemeStore = defineStore("theme", () => {
darkColor2: "#9ca3af",
darkColor3: "#6b7280",
colorTheme: currentThemeColor.value.primary,
})
});
// ==========================================================================
// 计算属性
// ==========================================================================
/** 是否为暗黑模式 */
const isDark = computed(() => theme.value === "dark")
const isDark = computed(() => theme.value === "dark");
// ==========================================================================
// 方法
@@ -59,41 +59,41 @@ export const useThemeStore = defineStore("theme", () => {
uni.setNavigationBarColor({
frontColor: theme.value === "light" ? "#000000" : "#ffffff",
backgroundColor: theme.value === "light" ? "#ffffff" : "#1f2937",
})
}
});
};
/**
* 切换主题
* @param mode 指定主题模式,不传则自动切换
*/
const toggleTheme = (mode?: ThemeMode) => {
theme.value = mode || (theme.value === "light" ? "dark" : "light")
Storage.set(THEME_MODE_KEY, theme.value)
setNavigationBarColor()
}
theme.value = mode || (theme.value === "light" ? "dark" : "light");
Storage.set(THEME_MODE_KEY, theme.value);
setNavigationBarColor();
};
/**
* 设置主题色
* @param color 主题色选项
*/
const setCurrentThemeColor = (color: ThemeColorOption) => {
currentThemeColor.value = color
Storage.set(THEME_COLOR_KEY, color)
themeVars.colorTheme = color.primary
}
currentThemeColor.value = color;
Storage.set(THEME_COLOR_KEY, color);
themeVars.colorTheme = color.primary;
};
/**
* 初始化主题
*/
const initTheme = () => {
// 更新主题变量中的颜色
themeVars.colorTheme = currentThemeColor.value.primary
themeVars.colorTheme = currentThemeColor.value.primary;
// 设置导航栏颜色
nextTick(() => {
setNavigationBarColor()
})
}
setNavigationBarColor();
});
};
// ==========================================================================
// 导出
@@ -113,5 +113,5 @@ export const useThemeStore = defineStore("theme", () => {
setCurrentThemeColor,
setNavigationBarColor,
initTheme,
}
})
};
});

View File

@@ -1,14 +1,14 @@
import { defineStore } from "pinia"
import { defineStore } from "pinia";
import AuthAPI, {
type LoginData,
type SmsLoginData,
type WxMaPhoneLoginData,
type WxMaBindMobileData,
} from "@/api/auth"
import UserAPI, { type UserInfo } from "@/api/user"
import { setAccessToken, clearTokens } from "@/utils/auth"
import { Storage } from "@/utils/storage"
import { USER_INFO_KEY } from "@/constants"
} from "@/api/auth";
import UserAPI, { type UserInfo } from "@/api/user";
import { setAccessToken, clearTokens } from "@/utils/auth";
import { Storage } from "@/utils/storage";
import { USER_INFO_KEY } from "@/constants";
/**
* 用户状态管理 Store
@@ -26,7 +26,7 @@ export const useUserStore = defineStore("user", () => {
// ==========================================================================
/** 用户信息 */
const userInfo = ref<UserInfo | undefined>(Storage.get<UserInfo>(USER_INFO_KEY))
const userInfo = ref<UserInfo | undefined>(Storage.get<UserInfo>(USER_INFO_KEY));
// ==========================================================================
// 登录方法
@@ -36,48 +36,48 @@ export const useUserStore = defineStore("user", () => {
* 账号密码登录
*/
const login = async (data: LoginData) => {
const result = await AuthAPI.login(data)
setAccessToken(result.accessToken)
return result
}
const result = await AuthAPI.login(data);
setAccessToken(result.accessToken);
return result;
};
/**
* 短信验证码登录
*/
const loginBySms = async (data: SmsLoginData) => {
const result = await AuthAPI.loginBySms(data)
setAccessToken(result.accessToken)
return result
}
const result = await AuthAPI.loginBySms(data);
setAccessToken(result.accessToken);
return result;
};
/**
* 微信小程序静默登录
*/
const loginByWxMa = async (code: string) => {
const result = await AuthAPI.wxMaSilentLogin(code)
const result = await AuthAPI.wxMaSilentLogin(code);
if (result.accessToken) {
setAccessToken(result.accessToken)
setAccessToken(result.accessToken);
}
return result
}
return result;
};
/**
* 微信小程序一键登录(企业小程序)
*/
const loginByWxMaPhone = async (data: WxMaPhoneLoginData) => {
const result = await AuthAPI.wxMaPhoneLogin(data)
setAccessToken(result.accessToken)
return result
}
const result = await AuthAPI.wxMaPhoneLogin(data);
setAccessToken(result.accessToken);
return result;
};
/**
* 微信小程序绑定手机号
*/
const bindMobileForWxMa = async (data: WxMaBindMobileData) => {
const result = await AuthAPI.wxMaBindMobile(data)
setAccessToken(result.accessToken)
return result
}
const result = await AuthAPI.wxMaBindMobile(data);
setAccessToken(result.accessToken);
return result;
};
// ==========================================================================
// 用户信息方法
@@ -88,46 +88,46 @@ export const useUserStore = defineStore("user", () => {
*/
const checkSession = async (): Promise<boolean> => {
try {
const result = await AuthAPI.checkSession()
return result.valid
const result = await AuthAPI.checkSession();
return result.valid;
} catch {
return false
return false;
}
}
};
/**
* 获取用户信息
*/
const getInfo = async () => {
const data = await UserAPI.getUserInfo()
Storage.set(USER_INFO_KEY, data)
userInfo.value = data
return data
}
const data = await UserAPI.getUserInfo();
Storage.set(USER_INFO_KEY, data);
userInfo.value = data;
return data;
};
/**
* 登出
*/
const logout = async () => {
try {
await AuthAPI.logout()
await AuthAPI.logout();
} catch {
// 登出失败静默处理,继续清理本地状态
} finally {
clearTokens()
Storage.remove(USER_INFO_KEY)
userInfo.value = undefined
uni.reLaunch({ url: "/pages/login/index" })
clearTokens();
Storage.remove(USER_INFO_KEY);
userInfo.value = undefined;
uni.reLaunch({ url: "/pages/login/index" });
}
}
};
/**
* 判断用户信息是否完整
*/
const isUserInfoComplete = (): boolean => {
if (!userInfo.value) return false
return !!(userInfo.value.nickname && userInfo.value.avatar)
}
if (!userInfo.value) return false;
return !!(userInfo.value.nickname && userInfo.value.avatar);
};
// ==========================================================================
// 导出
@@ -144,5 +144,5 @@ export const useUserStore = defineStore("user", () => {
getInfo,
checkSession,
isUserInfoComplete,
}
})
};
});

View File

@@ -58,8 +58,22 @@ page {
--color-border-light: #f3f4f6;
--color-border-dark: #d1d5db;
--color-border-glass: rgba(148, 163, 184, 0.22);
--color-border-glass-strong: rgba(255, 255, 255, 0.9);
--color-danger-shadow: rgba(239, 68, 68, 0.08);
// -----------------------------------------------------------------------
// 毛玻璃效果色
// -----------------------------------------------------------------------
--color-glass: rgba(255, 255, 255, 0.22);
--color-glass-light: rgba(255, 255, 255, 0.12);
--color-bg-alpha-95: rgba(255, 255, 255, 0.95);
// -----------------------------------------------------------------------
// 主题色透明度变体
// -----------------------------------------------------------------------
--color-primary-alpha-20: rgba(77, 128, 240, 0.2);
--color-primary-alpha-15: rgba(77, 128, 240, 0.15);
// -----------------------------------------------------------------------
// 阴影
// -----------------------------------------------------------------------
@@ -141,8 +155,22 @@ page {
--color-border-light: #1f2937;
--color-border-dark: #4b5563;
--color-border-glass: rgba(148, 163, 184, 0.15);
--color-border-glass-strong: rgba(255, 255, 255, 0.9);
--color-danger-shadow: rgba(239, 68, 68, 0.15);
// -----------------------------------------------------------------------
// 毛玻璃效果色
// -----------------------------------------------------------------------
--color-glass: rgba(255, 255, 255, 0.12);
--color-glass-light: rgba(255, 255, 255, 0.06);
--color-bg-alpha-95: rgba(31, 41, 55, 0.95);
// -----------------------------------------------------------------------
// 主题色透明度变体
// -----------------------------------------------------------------------
--color-primary-alpha-20: rgba(59, 130, 246, 0.2);
--color-primary-alpha-15: rgba(59, 130, 246, 0.15);
// -----------------------------------------------------------------------
// 阴影
// -----------------------------------------------------------------------