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

View File

@@ -211,8 +211,6 @@ function handleNodeAction(node: FlatNode) {
emit("action", node.raw); emit("action", node.raw);
} }
// ===== 多选相关 =====
/** 判断节点是否选中(多选模式) */ /** 判断节点是否选中(多选模式) */
function isChecked(value: string) { function isChecked(value: string) {
return internalCheckedKeys.value.has(value); 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 { export interface UseLoadingOptions {
/** 是否使用全局单例,默认 true */ /** 是否使用全局单例,默认 true */
singleton?: boolean singleton?: boolean;
/** 初始加载状态,默认 false */ /** 初始加载状态,默认 false */
initial?: boolean initial?: boolean;
/** 加载提示消息 */ /** 加载提示消息 */
message?: string message?: string;
} }
export interface UseLoadingReturn { 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 = {}) { function createLoadingState(options: UseLoadingOptions = {}) {
const { initial = false, message: initialMessage = "" } = options const { message: initialMessage = "" } = options;
const loadingCount = ref(0) const loadingCount = ref(0);
const message = ref(initialMessage) const message = ref(initialMessage);
const isLoading = computed(() => loadingCount.value > 0) const isLoading = computed(() => loadingCount.value > 0);
/** /**
* 开始加载 * 开始加载
* @param msg 加载提示消息 * @param msg 加载提示消息
*/ */
const start = (msg?: string): void => { const start = (msg?: string): void => {
loadingCount.value++ loadingCount.value++;
if (msg) { if (msg) {
message.value = msg message.value = msg;
} }
} };
/** /**
* 结束加载 * 结束加载
*/ */
const stop = (): void => { const stop = (): void => {
if (loadingCount.value > 0) { if (loadingCount.value > 0) {
loadingCount.value-- loadingCount.value--;
} }
if (loadingCount.value === 0) { 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> => { const wrap = async <T>(promise: Promise<T>): Promise<T> => {
try { try {
start() start();
return await promise return await promise;
} finally { } finally {
stop() stop();
} }
} };
/** /**
* 设置加载消息 * 设置加载消息
* @param msg 消息内容 * @param msg 消息内容
*/ */
const setMessage = (msg: string): void => { const setMessage = (msg: string): void => {
message.value = msg message.value = msg;
} };
return { return {
isLoading, isLoading,
@@ -113,11 +113,11 @@ function createLoadingState(options: UseLoadingOptions = {}) {
stop, stop,
wrap, wrap,
setMessage, setMessage,
} };
} }
/** 全局单例实例 */ /** 全局单例实例 */
let loadingInstance: UseLoadingReturn | null = null let loadingInstance: UseLoadingReturn | null = null;
/** /**
* 加载状态管理 Hook * 加载状态管理 Hook
@@ -128,14 +128,14 @@ let loadingInstance: UseLoadingReturn | null = null
* @param options 配置选项 * @param options 配置选项
*/ */
export function useLoading(options?: UseLoadingOptions): UseLoadingReturn { export function useLoading(options?: UseLoadingOptions): UseLoadingReturn {
const { singleton = true } = options ?? {} const { singleton = true } = options ?? {};
if (singleton) { if (singleton) {
if (!loadingInstance) { 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 配置选项 * @param options 配置选项
*/ */
export function useNavbar(options: UseNavbarOptions = {}): UseNavbarReturn { export function useNavbar(options: UseNavbarOptions = {}): UseNavbarReturn {
const { navBarHeight = 44, hasTabbar = false } = options; const { navBarHeight = 44 } = options;
// 状态栏高度 // 状态栏高度
const statusBarHeight = ref(0); 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> { export interface UseRequestOptions<T> {
/** 是否立即执行,默认 false */ /** 是否立即执行,默认 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 */ /** 是否重置数据在重新请求时,默认 false */
resetOnExecute?: boolean resetOnExecute?: boolean;
} }
export interface UseRequestReturn<T> { 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, onError,
onFinally, onFinally,
resetOnExecute = false, resetOnExecute = false,
} = options } = options;
// 使用 shallowRef 避免深层响应式,提升性能 // 使用 shallowRef 避免深层响应式,提升性能
const data = shallowRef<T | null>(initialData) as Ref<T | null> const data = shallowRef<T | null>(initialData) as Ref<T | null>;
const error = ref<Error | null>(null) const error = ref<Error | null>(null);
const loadingCount = ref(0) const loadingCount = ref(0);
const hasExecuted = ref(false) 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> => { const execute = async (): Promise<T | null> => {
// 重置状态 // 重置状态
if (resetOnExecute) { if (resetOnExecute) {
data.value = initialData data.value = initialData;
error.value = null error.value = null;
} }
loadingCount.value++ loadingCount.value++;
hasExecuted.value = true hasExecuted.value = true;
try { try {
onBefore?.() onBefore?.();
const result = await requestFn() const result = await requestFn();
data.value = result data.value = result;
error.value = null error.value = null;
onSuccess?.(result) onSuccess?.(result);
return result return result;
} catch (e) { } catch (e) {
const err = e instanceof Error ? e : new Error(String(e)) const err = e instanceof Error ? e : new Error(String(e));
error.value = err error.value = err;
onError?.(err) onError?.(err);
return null return null;
} finally { } finally {
loadingCount.value-- loadingCount.value--;
onFinally?.() onFinally?.();
} }
} };
/** /**
* 刷新数据 * 刷新数据
*/ */
const refresh = async (): Promise<T | null> => { const refresh = async (): Promise<T | null> => {
return execute() return execute();
} };
/** /**
* 重置状态 * 重置状态
*/ */
const reset = (): void => { const reset = (): void => {
data.value = initialData data.value = initialData;
error.value = null error.value = null;
loadingCount.value = 0 loadingCount.value = 0;
hasExecuted.value = false hasExecuted.value = false;
} };
// 立即执行 // 立即执行
if (immediate) { if (immediate) {
execute() execute();
} }
return { return {
@@ -145,7 +145,7 @@ export function useRequest<T>(
execute, execute,
refresh, refresh,
reset, reset,
} };
} }
/** /**
@@ -167,42 +167,42 @@ export function useRequest<T>(
*/ */
export interface PaginationParams { export interface PaginationParams {
pageNum: number pageNum: number;
pageSize: number pageSize: number;
} }
export interface UsePaginationOptions<T> { export interface UsePaginationOptions<T> {
/** 每页条数,默认 10 */ /** 每页条数,默认 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> { 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[]>>, requestFn: (params: PaginationParams) => Promise<PageResult<T[]>>,
options: UsePaginationOptions<T> = {} options: UsePaginationOptions<T> = {}
): UsePaginationReturn<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 list = ref<T[]>(initialData) as Ref<T[]>;
const loadingCount = ref(0) const loadingCount = ref(0);
const error = ref<Error | null>(null) const error = ref<Error | null>(null);
const pageNum = ref(1) const pageNum = ref(1);
const total = ref(0) const total = ref(0);
const loading = computed(() => loadingCount.value > 0) const loading = computed(() => loadingCount.value > 0);
const hasMore = computed(() => list.value.length < total.value) const hasMore = computed(() => list.value.length < total.value);
const isEmpty = computed(() => list.value.length === 0 && !loading.value) const isEmpty = computed(() => list.value.length === 0 && !loading.value);
/** /**
* 加载更多 * 加载更多
@@ -233,59 +233,59 @@ export function usePagination<T>(
const loadMore = async (): Promise<T[]> => { const loadMore = async (): Promise<T[]> => {
// 如果正在加载或没有更多数据,直接返回 // 如果正在加载或没有更多数据,直接返回
if (loading.value || !hasMore.value) { if (loading.value || !hasMore.value) {
return list.value return list.value;
} }
loadingCount.value++ loadingCount.value++;
try { try {
const result = await requestFn({ const result = await requestFn({
pageNum: pageNum.value, pageNum: pageNum.value,
pageSize, pageSize,
}) });
// 如果是第一页,替换数据;否则追加数据 // 如果是第一页,替换数据;否则追加数据
if (pageNum.value === 1) { if (pageNum.value === 1) {
list.value = result.list list.value = result.list;
} else { } else {
list.value = [...list.value, ...result.list] list.value = [...list.value, ...result.list];
} }
total.value = result.total total.value = result.total;
pageNum.value++ pageNum.value++;
onSuccess?.(result.list, result.total) onSuccess?.(result.list, result.total);
return result.list return result.list;
} catch (e) { } catch (e) {
const err = e instanceof Error ? e : new Error(String(e)) const err = e instanceof Error ? e : new Error(String(e));
error.value = err error.value = err;
onError?.(err) onError?.(err);
return [] return [];
} finally { } finally {
loadingCount.value-- loadingCount.value--;
} }
} };
/** /**
* 刷新(从第一页开始) * 刷新(从第一页开始)
*/ */
const refresh = async (): Promise<T[]> => { const refresh = async (): Promise<T[]> => {
pageNum.value = 1 pageNum.value = 1;
total.value = 0 total.value = 0;
list.value = [] list.value = [];
return loadMore() return loadMore();
} };
/** /**
* 重置 * 重置
*/ */
const reset = (): void => { const reset = (): void => {
list.value = initialData list.value = initialData;
loadingCount.value = 0 loadingCount.value = 0;
error.value = null error.value = null;
pageNum.value = 1 pageNum.value = 1;
total.value = 0 total.value = 0;
} };
return { return {
list, list,
@@ -298,5 +298,5 @@ export function usePagination<T>(
loadMore, loadMore,
refresh, refresh,
reset, reset,
} };
} }

View File

@@ -1,4 +1,4 @@
import { ref, computed } from "vue" import { ref, computed } from "vue";
/** /**
* TabBar 状态管理 * TabBar 状态管理
@@ -19,15 +19,15 @@
export interface TabbarItem { export interface TabbarItem {
/** 唯一标识 */ /** 唯一标识 */
name: string name: string;
/** 徽标数字,null 表示不显示 */ /** 徽标数字,null 表示不显示 */
value: number | null value: number | null;
/** 是否激活 */ /** 是否激活 */
active: boolean active: boolean;
/** 显示标题 */ /** 显示标题 */
title: string title: string;
/** 图标名称 */ /** 图标名称 */
icon: string icon: string;
} }
/** 默认 TabBar 配置 */ /** 默认 TabBar 配置 */
@@ -35,7 +35,7 @@ const DEFAULT_TABBAR_ITEMS: Omit<TabbarItem, "active">[] = [
{ name: "home", value: null, title: "首页", icon: "home" }, { name: "home", value: null, title: "首页", icon: "home" },
{ name: "work", value: null, title: "工作台", icon: "laptop" }, { name: "work", value: null, title: "工作台", icon: "laptop" },
{ name: "mine", value: null, title: "我的", icon: "user" }, { name: "mine", value: null, title: "我的", icon: "user" },
] ];
/** /**
* 创建 TabBar 状态实例 * 创建 TabBar 状态实例
@@ -49,24 +49,24 @@ function createTabbarState() {
...item, ...item,
active: index === 0, active: index === 0,
})) }))
) );
/** TabBar 列表 */ /** TabBar 列表 */
const tabbarList = computed(() => items.value) const tabbarList = computed(() => items.value);
/** 当前激活的 TabBar 项 */ /** 当前激活的 TabBar 项 */
const activeTabbar = computed(() => { const activeTabbar = computed(() => {
return items.value.find((item) => item.active) || items.value[0] return items.value.find((item) => item.active) || items.value[0];
}) });
/** /**
* 获取指定 TabBar 项的徽标数字 * 获取指定 TabBar 项的徽标数字
* @param name TabBar 项名称 * @param name TabBar 项名称
*/ */
const getTabbarItemValue = (name: string): number | null => { const getTabbarItemValue = (name: string): number | null => {
const item = items.value.find((item) => item.name === name) const item = items.value.find((item) => item.name === name);
return item?.value ?? null return item?.value ?? null;
} };
/** /**
* 设置 TabBar 项的徽标数字 * 设置 TabBar 项的徽标数字
@@ -74,11 +74,11 @@ function createTabbarState() {
* @param value 徽标数字 * @param value 徽标数字
*/ */
const setTabbarItem = (name: string, value: number): void => { 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) { if (item) {
item.value = value item.value = value;
} }
} };
/** /**
* 设置激活的 TabBar 项 * 设置激活的 TabBar 项
@@ -86,9 +86,9 @@ function createTabbarState() {
*/ */
const setTabbarItemActive = (name: string): void => { const setTabbarItemActive = (name: string): void => {
items.value.forEach((item) => { items.value.forEach((item) => {
item.active = item.name === name item.active = item.name === name;
}) });
} };
/** /**
* 重置 TabBar 状态 * 重置 TabBar 状态
@@ -97,8 +97,8 @@ function createTabbarState() {
items.value = DEFAULT_TABBAR_ITEMS.map((item, index) => ({ items.value = DEFAULT_TABBAR_ITEMS.map((item, index) => ({
...item, ...item,
active: index === 0, active: index === 0,
})) }));
} };
return { return {
tabbarList, tabbarList,
@@ -107,11 +107,11 @@ function createTabbarState() {
setTabbarItem, setTabbarItem,
setTabbarItemActive, setTabbarItemActive,
resetTabbar, resetTabbar,
} };
} }
/** 全局单例实例 */ /** 全局单例实例 */
let tabbarInstance: ReturnType<typeof createTabbarState> | null = null let tabbarInstance: ReturnType<typeof createTabbarState> | null = null;
/** /**
* TabBar 状态管理 Hook * TabBar 状态管理 Hook
@@ -123,14 +123,14 @@ let tabbarInstance: ReturnType<typeof createTabbarState> | null = null
* @param options.singleton 是否使用单例模式,默认 true * @param options.singleton 是否使用单例模式,默认 true
*/ */
export function useTabbar(options?: { singleton?: boolean }) { export function useTabbar(options?: { singleton?: boolean }) {
const { singleton = true } = options ?? {} const { singleton = true } = options ?? {};
if (singleton) { if (singleton) {
if (!tabbarInstance) { 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 LogAPI, { type VisitOverview as ApiVisitOverview, type VisitTrend } from "@/api/log";
import NoticeAPI, { type NoticeItem } from "@/api/notice"; import NoticeAPI, { type NoticeItem } from "@/api/notice";
// ============================================================================
// 类型定义
// ============================================================================
type VisitOverviewVO = ApiVisitOverview; type VisitOverviewVO = ApiVisitOverview;
interface NavItem { interface NavItem {
@@ -118,22 +114,17 @@ interface NavItem {
perm: string; perm: string;
} }
// ============================================================================
// Hooks
// ============================================================================
const router = useRouter(); const router = useRouter();
const userStore = useUserStore(); const userStore = useUserStore();
// custom-navbar 组件内部已处理导航栏高度与胶囊避让 // custom-navbar 组件内部已处理导航栏高度与胶囊避让
// ============================================================================
// 响应式数据
// ============================================================================
const current = ref(0); const current = ref(0);
const recentDaysRange = ref(7); 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>({ const visitOverviewData = ref<VisitOverviewVO>({
todayUvCount: 0, todayUvCount: 0,
@@ -228,10 +219,6 @@ const chartOpts = ref({
}, },
}); });
// ============================================================================
// 数据加载
// ============================================================================
function loadAppVersion() { function loadAppVersion() {
try { try {
const p: any = (globalThis as any).plus; const p: any = (globalThis as any).plus;
@@ -287,10 +274,6 @@ async function loadVisitTrendData() {
} }
} }
// ============================================================================
// 事件处理
// ============================================================================
function handleNavClick(item: NavItem) { function handleNavClick(item: NavItem) {
// 未登录 / 无权限时:展示默认导航,但点击统一跳登录 // 未登录 / 无权限时:展示默认导航,但点击统一跳登录
if (!isLogged.value || !hasAnyPerm.value) { if (!isLogged.value || !hasAnyPerm.value) {
@@ -332,10 +315,6 @@ function handleDataRangeChange({ value }: { value: number }) {
loadVisitTrendData(); loadVisitTrendData();
} }
// ============================================================================
// 生命周期
// ============================================================================
onReady(() => { onReady(() => {
loadAppVersion(); loadAppVersion();
loadNoticeData(); loadNoticeData();
@@ -491,13 +470,13 @@ onShow(() => {
&--uv, &--uv,
&--green { &--green {
background: #34d19d; background: var(--color-success);
box-shadow: 0 0 10rpx rgba(52, 209, 157, 0.35); box-shadow: 0 0 10rpx rgba(52, 209, 157, 0.35);
} }
&--pv, &--pv,
&--blue { &--blue {
background: #4d80f0; background: var(--color-primary);
box-shadow: 0 0 10rpx rgba(77, 128, 240, 0.3); box-shadow: 0 0 10rpx rgba(77, 128, 240, 0.3);
} }
} }
@@ -512,12 +491,12 @@ onShow(() => {
&--uv, &--uv,
&--green { &--green {
color: #34d19d; color: var(--color-success);
} }
&--pv, &--pv,
&--blue { &--blue {
color: #4d80f0; color: var(--color-primary);
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,9 +19,9 @@
> >
<!-- 自定义节点内容:ID + 名称 + 状态 --> <!-- 自定义节点内容:ID + 名称 + 状态 -->
<template #content="{ node }"> <template #content="{ node }">
<view class="flex-1 flex items-center gap-16rpx"> <view class="dept-node">
<text class="w-120rpx text-24rpx color-text-secondary">{{ node.id }}</text> <text class="dept-node__id">{{ node.id }}</text>
<text class="flex-1 truncate">{{ node.name }}</text> <text class="dept-node__name">{{ node.name }}</text>
<wd-tag :type="node.status === 1 ? 'success' : 'danger'" size="small"> <wd-tag :type="node.status === 1 ? 'success' : 'danger'" size="small">
{{ node.status === 1 ? "正常" : "禁用" }} {{ node.status === 1 ? "正常" : "禁用" }}
</wd-tag> </wd-tag>
@@ -72,10 +72,7 @@
</wd-popup> </wd-popup>
<!-- 浮动新增按钮 --> <!-- 浮动新增按钮 -->
<wd-fab <wd-fab v-if="hasPermission('sys:dept:create') && !dialog.visible" @click="openDeptDialog()" />
v-if="hasPermission('sys:dept:create') && !dialog.visible"
@click="openDeptDialog()"
/>
<!-- 操作菜单 --> <!-- 操作菜单 -->
<wd-action-sheet <wd-action-sheet
@@ -342,3 +339,25 @@ export default { options: { styleIsolation: "shared" } };
} }
} }
</route> </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" @select="handleActionSelect"
/> />
<wd-fab <wd-fab v-if="hasPermission('sys:dict:create') && !dialog.visible" @click="openDictDialog()" />
v-if="hasPermission('sys:dict:create') && !dialog.visible"
@click="openDictDialog()"
/>
</view> </view>
</template> </template>
@@ -96,16 +93,13 @@ import { onLoad, onReachBottom } from "@dcloudio/uni-app";
import { useRouter } from "uni-mini-router"; import { useRouter } from "uni-mini-router";
import { LoadMoreState } from "wot-design-uni/components/wd-loadmore/types"; import { LoadMoreState } from "wot-design-uni/components/wd-loadmore/types";
import { FormRules } from "wot-design-uni/components/wd-form/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 DictAPI, { import DictAPI, { type DictTypeForm, type DictTypePageQuery, type DictTypeItem } from "@/api/dict";
type DictTypeForm,
type DictTypePageQuery,
type DictTypeItem,
} from "@/api/dict";
import { hasPermission } from "@/utils/permission"; import { hasPermission } from "@/utils/permission";
const router = useRouter(); const router = useRouter();
const toast = useToast(); const toast = useToast();
const { messageBox } = useMessage();
const loadMoreState = ref<LoadMoreState>("loading"); const loadMoreState = ref<LoadMoreState>("loading");
const formRef = ref(); const formRef = ref();
const submitting = ref(false); const submitting = ref(false);
@@ -216,13 +210,19 @@ function showDictActions(item: DictTypeItem) {
actions.push({ name: "删除", color: "var(--color-danger)" }); actions.push({ name: "删除", color: "var(--color-danger)" });
actionMap["删除"] = async () => { actionMap["删除"] = async () => {
try { try {
await messageBox({ title: "确认删除", msg: `确定要删除字典「${item.name}」吗?`, type: "warning" }); await messageBox({
title: "确认删除",
msg: `确定要删除字典「${item.name}」吗?`,
type: "warning",
});
if (item.id) { if (item.id) {
await DictAPI.deleteByIds(String(item.id)); await DictAPI.deleteByIds(String(item.id));
toast.success("删除成功"); toast.success("删除成功");
loadDictTypeList(); loadDictTypeList();
} }
} catch {} } catch {
// 用户取消操作
}
}; };
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,11 +19,11 @@
> >
<!-- 主信息行 --> <!-- 主信息行 -->
<view class="flex-start"> <view class="flex-start">
<view class="flex-1"> <view class="role-card__main">
<view class="flex-start mt-12rpx"> <view class="flex-start mt-12rpx">
<text class="font-bold text-32rpx">{{ item.name }}</text> <text class="role-card__name">{{ item.name }}</text>
</view> </view>
<text class="text-24rpx color-text-secondary">{{ item.code }}</text> <text class="role-card__code">{{ item.code }}</text>
</view> </view>
<wd-tag :type="item.status === 1 ? 'success' : 'danger'" plain> <wd-tag :type="item.status === 1 ? 'success' : 'danger'" plain>
{{ item.status === 1 ? "正常" : "禁用" }} {{ item.status === 1 ? "正常" : "禁用" }}
@@ -31,23 +31,23 @@
</view> </view>
<!-- 辅助信息行 --> <!-- 辅助信息行 -->
<view class="flex gap-24rpx mt-12rpx"> <view class="role-card__meta">
<view class="flex-start min-w-0"> <view class="role-card__detail">
<wd-icon name="view" size="16" class="color-text-secondary" /> <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>
<view class="flex-start min-w-0"> <view class="role-card__detail">
<wd-icon name="sort" size="16" class="color-text-secondary" /> <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> </view>
<!-- 元信息行 --> <!-- 元信息行 -->
<view class="flex-between mt-16rpx"> <view class="role-card__footer">
<text class="text-24rpx color-text-placeholder">{{ item.createTime }}</text> <text class="role-card__time">{{ item.createTime }}</text>
<view <view
class="w-88rpx h-88rpx flex-center rounded-full" class="role-card__action"
hover-class="bg-[var(--color-text-placeholder)]/16" hover-class="role-card__action--hover"
@click.stop="showRoleActions(item)" @click.stop="showRoleActions(item)"
> >
<wd-icon name="more" size="18" class="color-text-secondary" /> <wd-icon name="more" size="18" class="color-text-secondary" />
@@ -96,10 +96,7 @@
</wd-popup> </wd-popup>
<!-- 浮动新增按钮 --> <!-- 浮动新增按钮 -->
<wd-fab <wd-fab v-if="hasPermission('sys:role:create') && !dialog.visible" @click="openRoleDialog()" />
v-if="hasPermission('sys:role:create') && !dialog.visible"
@click="openRoleDialog()"
/>
<!-- 操作菜单 --> <!-- 操作菜单 -->
<wd-action-sheet <wd-action-sheet
@@ -300,3 +297,62 @@ export default { options: { styleIsolation: "shared" } };
} }
} }
</route> </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"> <view class="flex-start">
<image class="w-80rpx h-80rpx rounded-full" :src="item.avatar" mode="aspectFill" /> <image class="user-card__avatar" :src="item.avatar" mode="aspectFill" />
<view class="flex-1 ml-16rpx"> <view class="user-card__main">
<view class="flex-start mt-12rpx"> <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 <wd-icon
v-if="item.gender === 1" v-if="item.gender === 1"
name="gender-male" name="gender-male"
@@ -69,9 +69,7 @@
class="ml-8rpx" class="ml-8rpx"
/> />
</view> </view>
<text class="text-24rpx color-text-secondary"> <text class="user-card__role">{{ item.roleNames }} · {{ item.deptName }}</text>
{{ item.roleNames }} · {{ item.deptName }}
</text>
</view> </view>
<wd-tag :type="item.status === 1 ? 'success' : 'danger'" plain> <wd-tag :type="item.status === 1 ? 'success' : 'danger'" plain>
{{ item.status === 1 ? "正常" : "禁用" }} {{ item.status === 1 ? "正常" : "禁用" }}
@@ -79,23 +77,23 @@
</view> </view>
<!-- 辅助信息行 --> <!-- 辅助信息行 -->
<view class="flex gap-24rpx mt-12rpx"> <view class="user-card__meta">
<view v-if="item.mobile" class="flex-start min-w-0"> <view v-if="item.mobile" class="user-card__contact">
<wd-icon name="mobile" size="16" class="color-text-secondary" /> <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>
<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" /> <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> </view>
<!-- 元信息行 --> <!-- 元信息行 -->
<view class="flex-between mt-16rpx"> <view class="user-card__footer">
<text class="text-24rpx color-text-placeholder">{{ item.createTime }}</text> <text class="user-card__time">{{ item.createTime }}</text>
<view <view
class="w-88rpx h-88rpx flex-center rounded-full" class="user-card__action"
hover-class="bg-[var(--color-text-placeholder)]/16" hover-class="user-card__action--hover"
@click.stop="showUserActions(item)" @click.stop="showUserActions(item)"
> >
<wd-icon name="more" size="18" class="color-text-secondary" /> <wd-icon name="more" size="18" class="color-text-secondary" />
@@ -157,10 +155,7 @@
</wd-popup> </wd-popup>
<!-- 浮动新增按钮 --> <!-- 浮动新增按钮 -->
<wd-fab <wd-fab v-if="hasPermission('sys:user:create') && !dialog.visible" @click="openUserDialog()" />
v-if="hasPermission('sys:user:create') && !dialog.visible"
@click="openUserDialog()"
/>
<!-- 操作菜单 --> <!-- 操作菜单 -->
<wd-action-sheet <wd-action-sheet
@@ -181,13 +176,22 @@
label="新密码" label="新密码"
placeholder="请输入新密码(至少6位)" placeholder="请输入新密码(至少6位)"
prop="password" prop="password"
:rules="[{ required: true, message: '请输入新密码' }, { pattern: /^.{6,}$/, message: '密码至少需要6位字符' }]" :rules="[
{ required: true, message: '请输入新密码' },
{ pattern: /^.{6,}$/, message: '密码至少需要6位字符' },
]"
/> />
</wd-cell-group> </wd-cell-group>
</wd-form> </wd-form>
<view class="popup-actions"> <view class="popup-actions">
<wd-button type="info" plain @click="resetPwdDialog.visible = false">取消</wd-button> <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>
</view> </view>
</wd-popup> </wd-popup>
@@ -198,13 +202,14 @@
import { onLoad, onReachBottom } from "@dcloudio/uni-app"; import { onLoad, onReachBottom } from "@dcloudio/uni-app";
import { LoadMoreState } from "wot-design-uni/components/wd-loadmore/types"; import { LoadMoreState } from "wot-design-uni/components/wd-loadmore/types";
import { FormRules } from "wot-design-uni/components/wd-form/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 UserAPI, { type UserPageQuery, UserItem, UserForm } from "@/api/user";
import RoleAPI from "@/api/role"; import RoleAPI from "@/api/role";
import DeptAPI from "@/api/dept"; import DeptAPI from "@/api/dept";
import { hasPermission } from "@/utils/permission"; import { hasPermission } from "@/utils/permission";
const toast = useToast(); const toast = useToast();
const { messageBox } = useMessage();
const { closeOutside } = useQueue(); const { closeOutside } = useQueue();
const loadMoreState = ref<LoadMoreState>("loading"); const loadMoreState = ref<LoadMoreState>("loading");
const formRef = ref(); const formRef = ref();
@@ -414,7 +419,11 @@ const actionSheetVisible = ref(false);
const actionSheetActions = ref<{ name: string; color?: string }[]>([]); const actionSheetActions = ref<{ name: string; color?: string }[]>([]);
const pendingAction = ref<Record<string, () => void>>({}); 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 resetPwdForm = reactive({ password: "" });
const resetPwdFormRef = ref(); const resetPwdFormRef = ref();
@@ -440,11 +449,17 @@ function showUserActions(item: UserItem) {
actions.push({ name: "删除", color: "var(--color-danger)" }); actions.push({ name: "删除", color: "var(--color-danger)" });
actionMap["删除"] = async () => { actionMap["删除"] = async () => {
try { try {
await messageBox({ title: "确认删除", msg: `确定要删除用户「${item.nickname}」吗?`, type: "warning" }); await messageBox({
title: "确认删除",
msg: `确定要删除用户「${item.nickname}」吗?`,
type: "warning",
});
await UserAPI.deleteByIds(String(item.id)); await UserAPI.deleteByIds(String(item.id));
toast.success("删除成功"); toast.success("删除成功");
loadUserList(); loadUserList();
} catch {} } catch {
// 用户取消操作
}
}; };
} }
@@ -515,3 +530,73 @@ export default { options: { styleIsolation: "shared" } };
} }
} }
</route> </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(); next();
}); });
router.afterEach((to, from) => { router.afterEach(() => {});
// 路由跳转日志(生产环境可通过 vite 配置自动移除)
});
export default router; export default router;

View File

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

View File

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

View File

@@ -58,8 +58,22 @@ page {
--color-border-light: #f3f4f6; --color-border-light: #f3f4f6;
--color-border-dark: #d1d5db; --color-border-dark: #d1d5db;
--color-border-glass: rgba(148, 163, 184, 0.22); --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-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-light: #1f2937;
--color-border-dark: #4b5563; --color-border-dark: #4b5563;
--color-border-glass: rgba(148, 163, 184, 0.15); --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-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);
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// 阴影 // 阴影
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------

View File

@@ -24,8 +24,16 @@ export default {
"flex-column": "flex flex-col", "flex-column": "flex flex-col",
"flex-row": "flex flex-row", "flex-row": "flex flex-row",
// 垂直布局并居中对齐
"flex-col-center": "flex flex-col items-center", "flex-col-center": "flex flex-col items-center",
"text-secondary": "color-text-secondary",
"text-placeholder": "color-text-placeholder",
"text-inverse": "color-text-inverse",
"text-xs": "text-24rpx",
"text-sm": "text-28rpx",
"text-base": "text-32rpx",
"text-lg": "text-36rpx",
"font-bold": "font-semibold",
}, },
], ],