feat(api): 添加重置用户密码接口

This commit is contained in:
Ray.Hao
2026-03-13 20:35:55 +08:00
parent 306d167a55
commit 24d1c3477b
21 changed files with 657 additions and 688 deletions

View File

@@ -143,6 +143,20 @@ const UserAPI = {
method: "DELETE",
});
},
/**
* 重置用户密码
*
* @param userId 用户ID
* @param password 新密码
*/
resetPassword(userId: number | string, password: string) {
return request({
url: `${USER_BASE_URL}/${userId}/password/reset`,
method: "PUT",
data: { password },
});
},
};
export default UserAPI;

View File

@@ -4,13 +4,6 @@ import { storeToRefs } from "pinia";
const themeStore = useThemeStore();
const { theme, themeVars } = storeToRefs(themeStore);
// 获取系统状态栏高度
const statusBarHeight = ref(0);
onMounted(() => {
const systemInfo = uni.getSystemInfoSync();
statusBarHeight.value = systemInfo.statusBarHeight || 0;
});
</script>
<script lang="ts">
@@ -24,25 +17,10 @@ export default {
</script>
<template>
<wd-config-provider
:theme-vars="themeVars"
:theme="theme"
:custom-class="`page-wraper ${theme}`"
:style="{
'--status-bar-height': statusBarHeight + 'px',
}"
>
<wd-config-provider :theme-vars="themeVars" :custom-class="theme" :theme="theme">
<slot />
<wd-notify />
<wd-toast />
<wd-message-box />
</wd-config-provider>
</template>
<style lang="scss" scoped>
.page-wraper {
box-sizing: border-box;
min-height: calc(100vh - var(--window-top));
background: var(--wot-color-bg);
}
</style>

View File

@@ -1,16 +1,8 @@
<template>
<wd-config-provider
:theme-vars="themeVars"
:custom-class="`page-wraper ${theme}`"
:theme="theme"
:style="{
'--status-bar-height': statusBarHeight + 'px',
}"
>
<wd-config-provider :theme-vars="themeVars" :custom-class="theme" :theme="theme">
<slot />
<wd-tabbar
:model-value="activeTabbar.name"
placeholder
bordered
safe-area-inset-bottom
fixed
@@ -43,9 +35,6 @@ const themeStore = useThemeStore();
const { themeVars, theme } = storeToRefs(themeStore);
const { activeTabbar, getTabbarItemValue, setTabbarItemActive, tabbarList } = useTabbar();
// 获取系统状态栏高度
const statusBarHeight = ref(0);
function handleTabbarChange({ value }: { value: string }) {
setTabbarItemActive(value);
router.pushTab({ name: value });
@@ -63,9 +52,6 @@ watch(
);
onMounted(() => {
const systemInfo = uni.getSystemInfoSync();
statusBarHeight.value = systemInfo.statusBarHeight || 0;
// #ifdef APP-PLUS
uni.hideTabBar();
// #endif
@@ -81,15 +67,3 @@ export default {
},
};
</script>
<style lang="scss">
.page-wraper {
box-sizing: border-box;
// 页面最小高度 = 视窗高度 - 状态栏高度
// --window-top: uni-app提供的状态栏高度CSS变量(包含状态栏+导航栏)
min-height: calc(100vh - var(--window-top));
background: var(--wot-color-bg);
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<view class="page">
<view class="page page--tabbar">
<!-- 轮播图 -->
<view class="hero">
<view class="relative">
<wd-swiper
v-model:current="current"
custom-class="swiper-box"
@@ -10,10 +10,11 @@
@click="handleSwiperClick"
@change="handleSwiperChange"
/>
<view class="hero-fade"></view>
</view>
<!-- 快捷导航 -->
<view class="section section--overlay">
<view class="section--overlay">
<wd-grid clickable :column="4">
<wd-grid-item
v-for="(item, index) in quickNavList"
@@ -30,7 +31,7 @@
</view>
<!-- 通知公告 -->
<view class="section">
<view class="m-24rpx">
<view class="notice-bar" @click="handleNoticeClick">
<view class="notice-bar__icon">
<wd-icon name="check-outline" size="32rpx" color="#34D19D" />
@@ -42,11 +43,11 @@
</view>
<!-- 数据统计 -->
<view class="section">
<view class="stat-grid">
<view class="stat-card stat-card--green">
<image class="stat-card__bg" src="/static/icons/visitor.svg" mode="aspectFit" />
<view class="stat-card__head">
<view class="m-24rpx">
<view class="grid grid-cols-2 gap-16rpx">
<view class="stat-card gradient-bg--success">
<image class="stat-card__icon" src="/static/icons/visitor.svg" mode="aspectFit" />
<view class="stat-card__header">
<text class="stat-card__label">访客数</text>
<view class="stat-card__dot stat-card__dot--green"></view>
</view>
@@ -54,9 +55,9 @@
{{ visitStatsData.todayUvCount }}
</text>
</view>
<view class="stat-card stat-card--blue">
<image class="stat-card__bg" src="/static/icons/browser.svg" mode="aspectFit" />
<view class="stat-card__head">
<view class="stat-card gradient-bg--primary">
<image class="stat-card__icon" src="/static/icons/browser.svg" mode="aspectFit" />
<view class="stat-card__header">
<text class="stat-card__label">浏览量</text>
<view class="stat-card__dot stat-card__dot--blue"></view>
</view>
@@ -64,22 +65,15 @@
{{ visitStatsData.todayPvCount }}
</text>
</view>
<view v-if="appVersion" class="stat-card stat-card--orange stat-card--full">
<view class="stat-card__head">
<text class="stat-card__label">应用版本</text>
<view class="stat-card__dot stat-card__dot--orange"></view>
</view>
<text class="stat-card__num stat-card__num--orange">{{ appVersion }}</text>
</view>
</view>
</view>
<!-- 访问趋势图表 -->
<view class="section">
<view class="m-24rpx">
<wd-card custom-class="chart-card">
<template #title>
<view class="chart-header">
<text class="chart-header__title">访问趋势</text>
<view class="flex-between">
<text class="text-28rpx font-semibold">访问趋势</text>
<wd-radio-group
v-model="recentDaysRange"
shape="button"
@@ -92,7 +86,7 @@
</view>
</template>
<view class="chart-container">
<view class="w-full h-600rpx">
<qiun-data-charts type="area" :chartData="chartData" :opts="chartOpts" />
</view>
</wd-card>
@@ -363,11 +357,7 @@ onReady(() => {
</route>
<style lang="scss" scoped>
.hero {
position: relative;
}
.hero::after {
.hero-fade {
position: absolute;
right: 0;
bottom: 0;
@@ -375,7 +365,6 @@ onReady(() => {
z-index: 1;
height: 120rpx;
pointer-events: none;
content: "";
background: linear-gradient(
to bottom,
rgba(245, 247, 250, 0) 0%,
@@ -384,67 +373,18 @@ onReady(() => {
);
}
:deep(.swiper-box) {
height: 420rpx;
overflow: hidden;
border-bottom-right-radius: 32rpx;
border-bottom-left-radius: 32rpx;
}
:deep(.swiper-box),
:deep(.swiper-box .wd-swiper__item),
:deep(.swiper-box image) {
height: 420rpx;
}
.navbar {
position: fixed;
top: 0;
right: 0;
left: 0;
z-index: 999;
background-color: rgba(255, 255, 255, 0.92);
-webkit-backdrop-filter: blur(12px);
backdrop-filter: blur(12px);
border-bottom: 1rpx solid rgba(0, 0, 0, 0.05);
box-shadow: 0 10rpx 24rpx rgba(0, 0, 0, 0.06);
&__content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16rpx;
}
&__left {
display: flex;
align-items: center;
}
&__title {
font-size: 34rpx;
font-weight: 600;
color: var(--color-text);
}
&__right {
display: flex;
align-items: center;
}
&__search {
width: 100%;
}
}
.section {
margin: 24rpx;
}
.section--overlay {
position: relative;
z-index: 2;
padding: 18rpx 8rpx;
margin-top: -140rpx;
margin: 24rpx;
margin-top: -120rpx;
background: var(--color-bg);
border-radius: 24rpx;
box-shadow: 0 16rpx 36rpx rgba(0, 0, 0, 0.08);
@@ -473,8 +413,8 @@ onReady(() => {
display: flex;
align-items: center;
padding: 24rpx 24rpx 24rpx 20rpx;
background: #fff;
border: 1rpx solid rgba(0, 0, 0, 0.04);
background: var(--color-bg);
border: 1rpx solid var(--color-border);
border-radius: 16rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
@@ -486,7 +426,7 @@ onReady(() => {
width: 48rpx;
height: 48rpx;
margin-right: 16rpx;
background: rgba(52, 209, 157, 0.1);
background: var(--color-success-light);
border-radius: 12rpx;
}
@@ -500,52 +440,25 @@ onReady(() => {
overflow: hidden;
font-size: 26rpx;
font-weight: 500;
color: #333;
color: var(--color-text);
-webkit-line-clamp: 1;
line-clamp: 1;
-webkit-box-orient: vertical;
}
}
.stat-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16rpx;
}
.stat-card {
position: relative;
display: flex;
flex-direction: column;
padding: 24rpx 20rpx;
overflow: hidden;
background: #fff;
border: 1rpx solid rgba(0, 0, 0, 0.04);
background: var(--color-bg);
border: 1rpx solid var(--color-border);
border-radius: 16rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
&--green {
background: linear-gradient(135deg, #f0fdf9 0%, #fff 100%);
}
&--blue {
background: linear-gradient(135deg, #f0f7ff 0%, #fff 100%);
}
&--orange {
background: linear-gradient(135deg, #fffbf0 0%, #fff 100%);
}
&__bg {
position: absolute;
right: -10rpx;
bottom: -10rpx;
width: 100rpx;
height: 100rpx;
opacity: 0.15;
}
&__head {
&__header {
position: relative;
z-index: 1;
display: flex;
@@ -557,7 +470,16 @@ onReady(() => {
&__label {
font-size: 24rpx;
font-weight: 500;
color: #999;
color: var(--color-text-secondary);
}
&__icon {
position: absolute;
right: -10rpx;
bottom: -10rpx;
width: 100rpx;
height: 100rpx;
opacity: 0.15;
}
&__dot {
@@ -590,18 +512,15 @@ onReady(() => {
letter-spacing: -1rpx;
&--green {
color: #2ab789;
text-shadow: 0 4rpx 12rpx rgba(42, 183, 137, 0.3);
color: var(--color-success);
}
&--blue {
color: #3a8ee6;
text-shadow: 0 4rpx 12rpx rgba(58, 142, 230, 0.3);
color: var(--color-primary);
}
&--orange {
color: #e8a838;
text-shadow: 0 4rpx 12rpx rgba(232, 168, 56, 0.3);
color: var(--color-warning);
}
}
@@ -612,45 +531,10 @@ onReady(() => {
justify-content: space-between;
padding: 20rpx 20rpx;
.stat-card__head {
margin-bottom: 0;
}
.stat-card__num {
font-size: 28rpx;
letter-spacing: 0;
}
}
}
.chart-header {
display: flex;
align-items: center;
justify-content: space-between;
&__title {
font-size: 28rpx;
font-weight: 600;
color: var(--color-text);
}
}
.chart-container {
width: 100%;
height: 300px;
margin-bottom: 40rpx;
}
:deep(.chart-card) {
margin: 0 !important;
}
:deep(.chart-card .wd-card) {
margin: 0 !important;
border-radius: 16rpx;
}
:deep(.chart-card .wd-card__body) {
padding: 24rpx !important;
}
</style>

View File

@@ -1,98 +1,343 @@
<template>
<view class="page dark:text-[var(--wot-color-text)]">
<view class="page page--padding">
<wd-card>
<view class="about-header">
<view class="flex items-center p-20rpx">
<wd-img
width="120rpx"
height="120rpx"
src="/static/logo.png"
mode="aspectFit"
class="about-logo"
class="mr-16rpx"
/>
<view class="about-meta">
<text class="app-title">vue-uniapp-template</text>
<view class="flex-1">
<text class="app-title">youlai-app</text>
<text class="app-version">版本 {{ version }}</text>
</view>
</view>
</wd-card>
<!-- 公司信息区域 -->
<wd-card custom-style="margin-top: 20rpx">
<view class="company-section">
<view class="section-title">
<wd-icon name="company" size="18" />
<text>有来开源组织</text>
</view>
<text class="section-subtitle">专注于快速构建和高效开发的应用解决方案</text>
<view class="px-20rpx pb-20rpx">
<text class="intro-title">项目介绍</text>
<text class="intro-desc">
一个基于 uni-app + Vue 3 + TypeScript 的移动端模板内置主题系统与 wot-design-uni
组件库同时提供管理端与多后端版本Java / Gin / Nest / Django / ThinkPHP /
ASP.NET并包含多租户实现
</text>
</view>
</wd-card>
<!-- 项目列表 -->
<wd-card title="优质项目" custom-style="margin-top: 20rpx">
<wd-cell-group border>
<wd-cell title="vue3-element-admin" icon="desktop">
<view slot="label" class="project-desc">
基于 Vue3 + Vite7 + TypeScript5 + Element-Plus + Pinia 构建的中后台管理模板
</view>
</wd-cell>
<wd-card title="仓库" custom-style="margin-top: 20rpx">
<wd-collapse v-model="collapseActive" accordion>
<wd-collapse-item title="Web 前端" name="web">
<wd-cell-group border>
<wd-cell
title="vue3-element-admin"
icon="desktop"
title-width="200px"
value="简介"
clickable
@click="openProjectIntro('vue3-element-admin')"
/>
<wd-cell
title="vue3-element-admin-js"
icon="desktop"
title-width="200px"
value="简介"
clickable
@click="openProjectIntro('vue3-element-admin-js')"
/>
<wd-cell
title="vue3-element-template"
icon="desktop"
title-width="200px"
value="简介"
clickable
@click="openProjectIntro('vue3-element-template')"
/>
</wd-cell-group>
</wd-collapse-item>
<wd-cell title="vue-uniapp-template" icon="mobile">
<view slot="label" class="project-desc">
基于 uni-app + Vue 3 + TypeScript集成多种工具的移动端应用模板
</view>
</wd-cell>
<wd-collapse-item title="移动端" name="mobile">
<wd-cell-group border>
<wd-cell
title="youlai-app"
icon="mobile"
title-width="200px"
value="简介"
clickable
@click="openProjectIntro('youlai-app')"
/>
</wd-cell-group>
</wd-collapse-item>
<wd-cell title="youlai-boot" icon="server">
<view slot="label" class="project-desc">
基于 Spring Boot 3 + Vue 3 构建的前后端分离单体权限管理系统
</view>
</wd-cell>
</wd-cell-group>
<wd-collapse-item title="后端6 种技术栈)" name="backend">
<wd-cell-group border>
<wd-cell
title="youlai-boot"
icon="server"
title-width="200px"
value="简介"
clickable
@click="openProjectIntro('youlai-boot')"
/>
<wd-cell
title="youlai-gin"
icon="server"
title-width="200px"
value="简介"
clickable
@click="openProjectIntro('youlai-gin')"
/>
<wd-cell
title="youlai-nest"
icon="server"
title-width="200px"
value="简介"
clickable
@click="openProjectIntro('youlai-nest')"
/>
<wd-cell
title="youlai-django"
icon="server"
title-width="200px"
value="简介"
clickable
@click="openProjectIntro('youlai-django')"
/>
<wd-cell
title="youlai-think"
icon="server"
title-width="200px"
value="简介"
clickable
@click="openProjectIntro('youlai-think')"
/>
<wd-cell
title="youlai-aspnet"
icon="server"
title-width="200px"
value="简介"
clickable
@click="openProjectIntro('youlai-aspnet')"
/>
</wd-cell-group>
</wd-collapse-item>
<wd-collapse-item title="多租户" name="tenant">
<wd-cell-group border>
<wd-cell
title="youlai-boot-tenant"
icon="server"
title-width="200px"
value="简介"
clickable
@click="openProjectIntro('youlai-boot-tenant')"
/>
</wd-cell-group>
</wd-collapse-item>
</wd-collapse>
</wd-card>
<!-- 联系方式 -->
<wd-card title="联系我们" custom-style="margin-top: 20rpx">
<wd-cell-group border>
<wd-cell title="官方网站" value="www.youlai.tech" icon="link" />
<wd-cell title="GitHub" value="github.com/youlaitech" icon="github" />
<wd-cell title="联系邮箱" value="youlaitech@163.com" icon="mail" />
<wd-cell
title="官方网站"
value="www.youlai.tech"
icon="link"
clickable
@click="openUrl('https://www.youlai.tech')"
/>
<wd-cell
title="GitHub"
value="github.com/youlaitech"
icon="github"
clickable
@click="openUrl('https://github.com/youlaitech')"
/>
<wd-cell
title="联系邮箱"
value="youlaitech@163.com"
icon="mail"
clickable
@click="copyText('youlaitech@163.com')"
/>
</wd-cell-group>
</wd-card>
<!-- 底部版权信息 -->
<view class="about-footer">
<text class="copyright">Copyright © {{ getYear() }} 有来开源组织</text>
<text class="copyright copyright-sub">All Rights Reserved</text>
<view class="py-32rpx text-center">
<text class="copyright">Copyright {{ getYear() }} 有来开源组织</text>
<text class="copyright mt-8rpx">All Rights Reserved</text>
</view>
<wd-popup v-model="introPopupVisible" position="bottom">
<view class="p-24rpx">
<view class="text-32rpx font-700">{{ currentProject?.title }}</view>
<view v-if="currentProject?.stack" class="mt-12rpx text-26rpx text-secondary">
技术栈{{ currentProject.stack }}
</view>
<view v-if="currentProject?.summary" class="mt-12rpx text-26rpx text-secondary">
{{ currentProject.summary }}
</view>
<view class="mt-20rpx">
<wd-button type="primary" block @click="goCurrentProjectRepo">去仓库</wd-button>
</view>
</view>
</wd-popup>
</view>
</template>
<script lang="ts" setup>
const version = ref("1.0.0");
const version = ref(import.meta.env.VITE_APP_VERSION);
const collapseActive = ref<string[]>(["web"]);
type ProjectIntroKey =
| "vue3-element-admin"
| "vue3-element-admin-js"
| "vue3-element-template"
| "youlai-app"
| "youlai-boot"
| "youlai-gin"
| "youlai-nest"
| "youlai-django"
| "youlai-think"
| "youlai-aspnet"
| "youlai-boot-tenant";
type ProjectIntroItem = {
title: string;
stack: string;
summary: string;
repoUrl: string;
};
const projectIntros: Record<ProjectIntroKey, ProjectIntroItem> = {
"vue3-element-admin": {
title: "vue3-element-admin",
stack: "Vue 3.5 + Vite 7 + TypeScript + Element Plus",
summary:
"极简开箱即用的企业级后台管理模板,内置用户/角色/菜单/部门等基础模块,支持动态路由与按钮/数据权限,并提供暗黑模式、国际化与代码生成等能力。",
repoUrl: "https://github.com/youlaitech/vue3-element-admin",
},
"vue3-element-admin-js": {
title: "vue3-element-admin-js",
stack: "Vue 3.5 + Vite 6 + JavaScript + Element Plus",
summary:
"vue3-element-admin 的 JavaScript 版本,保留核心后台能力与权限体系,适合更轻量的快速开发场景,并支持 Mock 与线上接口文档联调。",
repoUrl: "https://github.com/youlaitech/vue3-element-admin-js",
},
"vue3-element-template": {
title: "vue3-element-template",
stack: "Vue 3.5 + Vite 6 + TypeScript 5 + Element Plus + Pinia",
summary:
"从 vue3-element-admin 抽离出的精简中后台模板,预置工程化规范与常用业务基座,专注高频功能与更低的二次开发成本。",
repoUrl: "https://github.com/youlaitech/vue3-element-template",
},
"youlai-app": {
title: "youlai-app",
stack: "uni-app + Vue 3 + TypeScript + UnoCSS + wot-design-uni",
summary:
"跨平台移动端模板,内置代码规范与工程化工具链,适配 H5 与微信小程序等多端场景,并提供可复用的基础/业务组件体系。",
repoUrl: "https://github.com/youlaitech/youlai-app",
},
"youlai-boot": {
title: "youlai-boot",
stack: "JDK 17 + Spring Boot 4 + Spring Security + JWT + Redis + MySQL",
summary:
"配套 vue3-element-admin 的 Java 后端实现,提供 RBAC 权限体系与用户/角色/菜单等核心模块,支持令牌续期与多端会话治理,并内置代码生成能力。",
repoUrl: "https://github.com/youlaitech/youlai-boot",
},
"youlai-gin": {
title: "youlai-gin",
stack: "Go 1.21+ + Gin + GORM + JWT + Redis + MySQL",
summary:
"配套 vue3-element-admin 的 Go/Gin 后端实现,轻量高性能,提供 RBAC 权限模型与常用系统模块,支持 JWT 与 Redis 会话管理。",
repoUrl: "https://github.com/youlaitech/youlai-gin",
},
"youlai-nest": {
title: "youlai-nest",
stack: "Node.js 20+ + NestJS 11 + TypeScript + TypeORM + JWT + Redis + MySQL",
summary:
"配套 vue3-element-admin 的 Node.js 后端实现,基于 NestJS 的企业级架构,支持 JWT 与 Redis Token 两种会话模式,并提供完整的 RBAC 权限与核心系统模块。",
repoUrl: "https://github.com/youlaitech/youlai-nest",
},
"youlai-django": {
title: "youlai-django",
stack: "Python 3.12+ + Django 6 + DRF + SimpleJWT + Redis + MySQL",
summary:
"配套 vue3-element-admin 的 Python 后端实现,基于 DRF 构建,提供 RBAC 权限模型与核心系统模块,采用 JWT 认证并结合 Redis 管理会话。",
repoUrl: "https://github.com/youlaitech/youlai-django",
},
"youlai-think": {
title: "youlai-think",
stack: "PHP 8 + ThinkPHP 8 + JWT + Redis + MySQL",
summary:
"配套 vue3-element-admin 的 PHP 后端实现,上手成本低,提供 RBAC 权限体系与常用系统模块,支持 JWT 与 Redis 会话双模式。",
repoUrl: "https://github.com/youlaitech/youlai-think",
},
"youlai-aspnet": {
title: "youlai-aspnet",
stack: ".NET 8 + ASP.NET Core 8 + JWT + Redis + MySQL",
summary:
"配套 vue3-element-admin 的 .NET 后端实现,性能与生态同步升级,提供 RBAC 权限治理与核心系统模块,并支持 JWT 与 Redis 会话模式。",
repoUrl: "https://github.com/youlaitech/youlai-aspnet",
},
"youlai-boot-tenant": {
title: "youlai-boot-tenant",
stack: "JDK 17 + Spring Boot 3 + Spring Security + MyBatis-Plus + JWT + Redis + MySQL",
summary:
"youlai-boot 的多租户版本,基于单库多租户方案实现租户隔离,提供精细化 RBAC 权限控制,适合 SaaS 场景的快速落地。",
repoUrl: "https://github.com/youlaitech/youlai-boot-tenant",
},
};
const introPopupVisible = ref(false);
const currentProjectKey = ref<ProjectIntroKey>("vue3-element-admin");
const currentProject = computed(() => projectIntros[currentProjectKey.value]);
const openProjectIntro = (key: ProjectIntroKey) => {
currentProjectKey.value = key;
introPopupVisible.value = true;
};
const goCurrentProjectRepo = () => {
if (!currentProject.value) return;
openUrl(currentProject.value.repoUrl);
};
const getYear = () => {
return new Date().getFullYear();
};
const openUrl = (url: string) => {
// #ifdef H5
window.open(url, "_blank");
// #endif
// #ifndef H5
uni.setClipboardData({ data: url });
// #endif
};
const copyText = (text: string) => {
uni.setClipboardData({ data: text });
};
onMounted(() => {
// #ifdef MP-WEIXIN
version.value = uni.getSystemInfoSync().appVersion;
const appVersion = uni.getSystemInfoSync().appVersion;
if (appVersion) {
version.value = appVersion;
}
// #endif
});
</script>
<style lang="scss" scoped>
.about-header {
display: flex;
align-items: center;
padding: 20rpx;
}
.about-logo {
margin-right: 16rpx;
}
.about-meta {
flex: 1;
.page {
:deep(.wd-cell__title) {
max-width: 360rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.app-title {
@@ -103,12 +348,23 @@ onMounted(() => {
}
.app-version {
display: block;
font-size: 24rpx;
color: #6b7280;
color: var(--color-text-secondary);
}
.company-section {
padding: 24rpx;
.intro-title {
display: block;
margin-bottom: 8rpx;
font-size: 28rpx;
font-weight: 500;
}
.intro-desc {
display: block;
font-size: 24rpx;
line-height: 1.6;
color: var(--color-text-secondary);
}
.section-title {
@@ -127,27 +383,12 @@ onMounted(() => {
display: block;
padding-left: 32rpx;
font-size: 24rpx;
color: #6b7280;
}
.project-desc {
margin-top: 8rpx;
font-size: 24rpx;
color: #6b7280;
}
.about-footer {
padding: 32rpx 0;
text-align: center;
color: var(--color-text-secondary);
}
.copyright {
display: block;
font-size: 24rpx;
color: #9ca3af;
}
.copyright-sub {
margin-top: 8rpx;
color: var(--color-text-placeholder);
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<view class="page dark:text-[var(--wot-color-text)]">
<view class="page page--tabbar">
<view class="mine-hero" :style="{ paddingTop: `${navbar.totalHeight.value}px` }">
<!-- 蓝色背景 -->
<view class="mine-hero__bg" :style="{ background: headerBackground }" />
@@ -100,19 +100,23 @@
</view>
</view>
<view v-if="quickCards.length" class="quick-cards">
<view
v-for="item in quickCards"
:key="item.title"
class="quick-card"
@click="item.action"
>
<view class="quick-card__icon" :style="{ background: item.tint }">
<wd-icon :name="item.icon" size="28" :color="item.iconColor || '#1e293b'" />
<view class="quick-cards">
<view class="quick-card" @click="openProfile">
<view class="quick-card__icon" style="background: #dbeafe">
<wd-icon name="user" size="28" color="#1e40af" />
</view>
<view class="quick-card__body">
<text class="quick-card__title">{{ item.title }}</text>
<text class="quick-card__desc">{{ item.desc }}</text>
<text class="quick-card__title">我的资料</text>
<text class="quick-card__desc">修改个人信息</text>
</view>
</view>
<view class="quick-card" @click="openAccount">
<view class="quick-card__icon" style="background: #d1fae5">
<wd-icon name="secured" size="28" color="#065f46" />
</view>
<view class="quick-card__body">
<text class="quick-card__title">账号安全</text>
<text class="quick-card__desc">修改密码 / 账号绑定</text>
</view>
</view>
</view>
@@ -123,20 +127,25 @@
<view class="section-card">
<text class="section-title">系统工具</text>
<view class="menu-list menu-list--flat">
<view
v-for="item in settingsItems"
:key="item.title"
class="menu-row"
@click="item.action"
>
<view class="menu-row__icon" :style="{ background: item.tint }">
<wd-icon :name="item.icon" size="18" :color="item.iconColor || '#1e293b'" />
<view class="menu-row" @click="openNetworkTest">
<view class="menu-row__icon" style="background: #fed7aa">
<wd-icon name="server" size="18" color="#c2410c" />
</view>
<view class="menu-row__main">
<text class="menu-row__title">{{ item.title }}</text>
<text class="menu-row__desc">{{ item.desc }}</text>
<text class="menu-row__title">网络检测</text>
<text class="menu-row__desc">检测接口连通性</text>
</view>
<text v-if="item.value" class="menu-row__value">{{ item.value }}</text>
<wd-icon name="arrow-right" size="16" color="#94a3b8" />
</view>
<view class="menu-row" @click="handleClearCache">
<view class="menu-row__icon" style="background: #fecdd3">
<wd-icon name="delete-thin" size="18" color="#be123c" />
</view>
<view class="menu-row__main">
<text class="menu-row__title">清理缓存</text>
<text class="menu-row__desc">显示当前缓存大小</text>
</view>
<text class="menu-row__value">{{ cacheSize }}</text>
<wd-icon name="arrow-right" size="16" color="#94a3b8" />
</view>
</view>
@@ -145,15 +154,25 @@
<view class="section-card">
<text class="section-title">帮助与支持</text>
<view class="menu-list menu-list--flat">
<view v-for="item in helpItems" :key="item.title" class="menu-row" @click="item.action">
<view class="menu-row__icon" :style="{ background: item.tint }">
<wd-icon :name="item.icon" size="18" :color="item.iconColor || '#1e293b'" />
<view class="menu-row" @click="openUserAgreement">
<view class="menu-row__icon" style="background: #d1fae5">
<wd-icon name="secured" size="18" color="#065f46" />
</view>
<view class="menu-row__main">
<text class="menu-row__title">{{ item.title }}</text>
<text class="menu-row__desc">{{ item.desc }}</text>
<text class="menu-row__title">用户协议</text>
<text class="menu-row__desc">了解产品使用规则</text>
</view>
<text v-if="item.value" class="menu-row__value">{{ item.value }}</text>
<wd-icon name="arrow-right" size="16" color="#94a3b8" />
</view>
<view class="menu-row" @click="openAbout">
<view class="menu-row__icon" style="background: #ccfbf1">
<wd-icon name="info-circle" size="18" color="#0f766e" />
</view>
<view class="menu-row__main">
<text class="menu-row__title">关于系统</text>
<text class="menu-row__desc">产品介绍与联系方式</text>
</view>
<text class="menu-row__value">v{{ appVersion }}</text>
<wd-icon name="arrow-right" size="16" color="#94a3b8" />
</view>
</view>
@@ -426,83 +445,6 @@ const openOfficialAccount = () => {
url: "/pages/mine/official/index",
});
};
type ActionItem = {
title: string;
desc?: string;
icon: string;
tint: string;
iconColor?: string;
value?: string;
action: () => void;
};
const quickCards = computed<ActionItem[]>(() => {
const items: ActionItem[] = [
{
title: "我的资料",
desc: "修改个人信息",
icon: "user",
tint: "#dbeafe",
iconColor: "#1e40af",
action: openProfile,
},
{
title: "账号安全",
desc: "修改密码 / 账号绑定",
icon: "secured",
tint: "#d1fae5",
iconColor: "#065f46",
action: openAccount,
},
];
if (isLogin.value) {
return items;
}
return [];
});
const settingsItems = computed<ActionItem[]>(() => [
{
title: "网络检测",
desc: "检测接口连通性",
icon: "server",
tint: "#fed7aa",
iconColor: "#c2410c",
action: openNetworkTest,
},
{
title: "清理缓存",
desc: "显示当前缓存大小",
icon: "delete-thin",
tint: "#fecdd3",
iconColor: "#be123c",
value: cacheSize.value,
action: handleClearCache,
},
]);
const helpItems = computed<ActionItem[]>(() => [
{
title: "用户协议",
desc: "了解产品使用规则",
icon: "secured",
tint: "#d1fae5",
iconColor: "#065f46",
action: openUserAgreement,
},
{
title: "关于系统",
desc: "产品介绍与联系方式",
icon: "info-circle",
tint: "#ccfbf1",
iconColor: "#0f766e",
value: `v${appVersion.value}`,
action: openAbout,
},
]);
</script>
<style lang="scss" scoped>
@@ -839,7 +781,7 @@ const helpItems = computed<ActionItem[]>(() => [
margin-top: 14rpx;
font-size: 32rpx;
font-weight: 600;
color: var(--wot-color-text, #2f3a4a);
color: var(--color-text);
letter-spacing: 0.2px;
}
@@ -854,7 +796,7 @@ const helpItems = computed<ActionItem[]>(() => [
overflow: hidden;
font-size: 24rpx;
line-height: 1.55;
color: var(--wot-color-text-secondary, #64748b);
color: var(--color-text-secondary);
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
@@ -862,7 +804,7 @@ const helpItems = computed<ActionItem[]>(() => [
.section-card {
padding: 28rpx;
background: var(--wot-color-white, #fff);
background: var(--color-bg);
border-radius: 32rpx;
box-shadow: 0 10rpx 30rpx rgba(15, 23, 42, 0.05);
}
@@ -876,7 +818,7 @@ const helpItems = computed<ActionItem[]>(() => [
margin-bottom: 18rpx;
font-size: 26rpx;
font-weight: 600;
color: var(--wot-color-text, #2f3a4a);
color: var(--color-text);
}
.quick-card {
@@ -884,7 +826,7 @@ const helpItems = computed<ActionItem[]>(() => [
gap: 20rpx;
align-items: center;
padding: 28rpx 24rpx;
background: var(--wot-color-white, #fff);
background: var(--color-bg);
border-radius: 24rpx;
box-shadow: 0 8rpx 24rpx rgba(15, 23, 42, 0.04);
transition: all 0.2s ease;
@@ -915,7 +857,7 @@ const helpItems = computed<ActionItem[]>(() => [
font-size: 28rpx;
font-weight: 500;
line-height: 1.35;
color: var(--wot-color-text, #2f3a4a);
color: var(--color-text);
text-overflow: ellipsis;
white-space: nowrap;
}
@@ -926,13 +868,14 @@ const helpItems = computed<ActionItem[]>(() => [
overflow: hidden;
font-size: 22rpx;
line-height: 1.6;
color: var(--wot-color-text-secondary, #64748b);
color: var(--color-text-secondary);
text-overflow: ellipsis;
white-space: nowrap;
}
.menu-row__icon {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 72rpx;
@@ -945,7 +888,7 @@ const helpItems = computed<ActionItem[]>(() => [
z-index: 0;
padding: 0 28rpx calc(env(safe-area-inset-bottom) + 40rpx);
margin-top: 0;
background: var(--wot-color-bg, #f8fafc);
background: var(--color-bg-secondary);
border-top-left-radius: 48rpx;
border-top-right-radius: 48rpx;
}
@@ -954,7 +897,7 @@ const helpItems = computed<ActionItem[]>(() => [
padding: 0 28rpx;
margin-top: 24rpx;
overflow: hidden;
background: var(--wot-color-white, #fff);
background: var(--color-bg);
border-radius: 32rpx;
box-shadow: 0 16rpx 40rpx rgba(15, 23, 42, 0.05);
}
@@ -978,12 +921,6 @@ const helpItems = computed<ActionItem[]>(() => [
border-top: 1rpx solid rgba(148, 163, 184, 0.14);
}
.menu-row__icon {
flex-shrink: 0;
width: 76rpx;
height: 76rpx;
}
.menu-row__main {
flex: 1;
min-width: 0;
@@ -993,20 +930,20 @@ const helpItems = computed<ActionItem[]>(() => [
display: block;
font-size: 26rpx;
font-weight: 500;
color: var(--wot-color-text, #2f3a4a);
color: var(--color-text);
}
.menu-row__desc {
display: block;
margin-top: 8rpx;
font-size: 22rpx;
color: var(--wot-color-text-secondary, #64748b);
color: var(--color-text-secondary);
}
.menu-row__value {
margin-left: 12rpx;
font-size: 22rpx;
color: #94a3b8;
color: var(--color-text-placeholder);
}
.logout-section {
@@ -1028,8 +965,8 @@ const helpItems = computed<ActionItem[]>(() => [
width: 100%;
height: 88rpx !important;
font-size: 28rpx !important;
color: #ef4444 !important;
background: var(--wot-color-white, #fff) !important;
color: var(--color-danger) !important;
background: var(--color-bg) !important;
border: 1rpx solid rgba(239, 68, 68, 0.18) !important;
border-radius: 24rpx !important;
box-shadow: 0 12rpx 30rpx rgba(239, 68, 68, 0.08);

View File

@@ -1,131 +1,105 @@
<template>
<view class="page-container dark:text-[var(--wot-color-text)]">
<view class="page page--padding">
<!-- 暗黑模式设置 -->
<wd-card custom-class="mx-30rpx my-20rpx">
<template #header>
<view class="flex-start">
<wd-icon name="moon" size="20" />
<text class="ml-12rpx text-28rpx font-600">外观模式</text>
</view>
</template>
<wd-cell-group title="外观模式" border>
<wd-cell title="暗黑模式" :value="isDarkMode ? '已开启' : '已关闭'">
<wd-switch :model-value="isDarkMode" @change="handleToggleDarkMode" />
</wd-cell>
</wd-card>
</wd-cell-group>
<!-- 主题色设置 -->
<wd-card custom-class="mx-30rpx my-20rpx">
<template #header>
<view class="flex-start">
<wd-icon name="palette" size="20" />
<text class="ml-12rpx text-28rpx font-600">主题色彩</text>
</view>
</template>
<wd-cell-group title="主题色彩" border custom-class="mt-24rpx">
<!-- 预设颜色选择 -->
<view class="theme-colors">
<view class="theme-colors__label">预设主题色</view>
<wd-grid :column="3" border>
<wd-grid-item
v-for="(color, index) in themeColorOptions"
:key="index"
@click="handleSelectColor(color)"
>
<view class="theme-colors__item">
<view
class="theme-colors__preview"
:class="{ 'theme-colors__preview--active': currentThemeColor === color.primary }"
:style="{ backgroundColor: color.primary }"
>
<text v-if="currentThemeColor === color.primary" class="theme-colors__check">
</text>
</view>
<text class="theme-colors__name">{{ color.name }}</text>
</view>
</wd-grid-item>
</wd-grid>
</view>
<!-- 当前主题色显示 -->
<view class="current-theme">
<view class="current-theme__item">
<view class="current-theme__info">
<wd-cell title="预设主题色">
<template #default>
<view class="flex gap-16rpx">
<view
class="current-theme__preview"
:style="{ backgroundColor: currentThemeColor }"
></view>
<text class="current-theme__label">当前主题色</text>
v-for="(color, index) in themeColorOptions"
:key="index"
class="color-option"
:class="{ 'color-option--active': currentThemeColor === color.primary }"
:style="{ backgroundColor: color.primary }"
@click="handleSelectColor(color)"
>
<wd-icon
v-if="currentThemeColor === color.primary"
name="check"
size="14"
color="#fff"
/>
</view>
</view>
<text class="current-theme__value">{{ currentThemeColor }}</text>
</view>
</view>
</template>
</wd-cell>
<!-- 当前主题色 -->
<wd-cell title="当前主题色">
<template #default>
<view class="flex items-center gap-12rpx">
<view class="color-dot" :style="{ backgroundColor: currentThemeColor }" />
<text class="text-24rpx color-text-secondary">{{ currentThemeColor }}</text>
</view>
</template>
</wd-cell>
<!-- 自定义颜色 -->
<wd-cell title="自定义颜色" is-link @click="showCustomInput">
<template #icon>
<wd-icon name="edit" size="16" />
<wd-icon name="edit" size="16" class="mr-12rpx" />
</template>
</wd-cell>
</wd-card>
</wd-cell-group>
<!-- 效果预览 -->
<wd-card title="效果预览" custom-class="mx-30rpx my-20rpx">
<wd-grid :column="3" border>
<wd-grid-item>
<view class="flex-col-center py-20rpx">
<wd-cell-group title="效果预览" border custom-class="mt-24rpx">
<wd-cell>
<template #default>
<view class="flex gap-24rpx py-12rpx">
<wd-button type="primary" size="small">主要按钮</wd-button>
</view>
</wd-grid-item>
<wd-grid-item>
<view class="flex-col-center py-20rpx">
<text class="text-28rpx font-500" :style="{ color: currentThemeColor }">
主题色文本
</text>
</view>
</wd-grid-item>
<wd-grid-item>
<view class="flex-col-center py-20rpx">
<wd-tag type="primary" size="small">标签</wd-tag>
</view>
</wd-grid-item>
</wd-grid>
</wd-card>
</template>
</wd-cell>
</wd-cell-group>
<!-- 操作按钮 -->
<view class="p-30rpx">
<view class="mt-32rpx">
<wd-button type="info" size="large" block @click="handleResetTheme">重置为默认主题</wd-button>
</view>
<!-- 自定义颜色输入弹窗 -->
<wd-popup v-model="showCustomColorInput" position="bottom" :safe-area-inset-bottom="true">
<view class="custom-color-popup">
<view class="custom-color-popup__header">
<text class="custom-color-popup__title">自定义主题色</text>
<wd-icon name="close" size="20" @click="showCustomColorInput = false" />
</view>
<view class="custom-color-popup__content">
<text class="custom-color-popup__label">请输入十六进制颜色值</text>
<view class="custom-color-popup__input-row">
<view
class="custom-color-popup__preview"
:style="{ backgroundColor: customColor || '#FF4757' }"
></view>
<wd-input
v-model="customColor"
placeholder="例如: #FF4757"
:maxlength="7"
custom-class="custom-color-popup__input"
/>
<wd-popup
v-model="showCustomColorInput"
position="bottom"
custom-style="border-radius: 24rpx 24rpx 0 0"
>
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">自定义主题色</text>
<view
class="w-64rpx h-64rpx flex-center rounded-full"
hover-class="bg-[var(--color-text-placeholder)]/16"
@click="showCustomColorInput = false"
>
<wd-icon name="close" size="18" class="color-text-secondary" />
</view>
<text class="custom-color-popup__tip">支持格式#RGB #RRGGBB</text>
</view>
<view class="custom-color-popup__actions">
<wd-button type="info" size="large" @click="showCustomColorInput = false">取消</wd-button>
<wd-button type="primary" size="large" @click="applyCustomColor">应用</wd-button>
<view class="p-4">
<text class="text-28rpx color-text-secondary">请输入十六进制颜色值</text>
<view class="flex items-center gap-16rpx mt-16rpx">
<view class="color-dot-lg" :style="{ backgroundColor: customColor || '#FF4757' }" />
<wd-input v-model="customColor" placeholder="例如: #FF4757" :maxlength="7" />
</view>
<text class="text-24rpx color-text-placeholder mt-12rpx">支持格式#RGB #RRGGBB</text>
</view>
<view class="popup-actions">
<wd-button type="info" plain @click="showCustomColorInput = false">取消</wd-button>
<wd-button type="primary" @click="applyCustomColor">应用</wd-button>
</view>
</view>
</wd-popup>
@@ -262,152 +236,57 @@ onShow(() => {
</route>
<style lang="scss" scoped>
// 主题颜色选择区域
.theme-colors {
padding: 30rpx;
.color-option {
display: flex;
align-items: center;
justify-content: center;
width: 48rpx;
height: 48rpx;
cursor: pointer;
border-radius: 50%;
transition: transform 0.2s;
&__label {
display: block;
margin-bottom: 20rpx;
font-size: 28rpx;
color: var(--wot-color-text-secondary);
}
&__item {
display: flex;
flex-direction: column;
align-items: center;
padding: 10rpx;
}
&__preview {
display: flex;
align-items: center;
justify-content: center;
width: 60rpx;
height: 60rpx;
margin-bottom: 8rpx;
border: 2rpx solid var(--wot-color-border);
border-radius: 12rpx;
transition: all 0.3s ease;
&--active {
border-color: var(--wot-color-theme);
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.2);
transform: scale(1.05);
}
}
&__check {
font-size: 24rpx;
font-weight: bold;
color: #fff;
text-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.5);
}
&__name {
font-size: 22rpx;
color: var(--wot-color-text-secondary);
text-align: center;
&--active {
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.2);
transform: scale(1.1);
}
}
// 当前主题色显示区域
.current-theme {
padding: 30rpx;
border-bottom: 1rpx solid var(--wot-color-border);
&__item {
display: flex;
align-items: center;
justify-content: space-between;
}
&__info {
display: flex;
align-items: center;
}
&__preview {
width: 40rpx;
height: 40rpx;
margin-right: 12rpx;
border: 2rpx solid var(--wot-color-border);
border-radius: 8rpx;
}
&__label {
font-size: 28rpx;
color: var(--wot-color-text);
}
&__value {
font-size: 28rpx;
font-weight: 500;
color: var(--wot-color-text-secondary);
}
.color-dot {
width: 32rpx;
height: 32rpx;
border-radius: 50%;
}
// 自定义颜色弹窗
.custom-color-popup {
min-height: 400rpx;
background-color: var(--wot-color-bg);
border-radius: 20rpx 20rpx 0 0;
.color-dot-lg {
flex-shrink: 0;
width: 48rpx;
height: 48rpx;
border-radius: 8rpx;
}
&__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 1rpx solid var(--wot-color-border);
}
.popup-content {
padding-bottom: env(safe-area-inset-bottom);
}
&__title {
font-size: 32rpx;
font-weight: 600;
color: var(--wot-color-text);
}
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 32rpx;
border-bottom: 1rpx solid var(--color-border);
}
&__content {
padding: 40rpx 30rpx;
}
.popup-title {
font-size: 32rpx;
font-weight: 600;
color: var(--color-text);
}
&__label {
display: block;
margin-bottom: 20rpx;
font-size: 28rpx;
color: var(--wot-color-text);
}
&__input-row {
display: flex;
gap: 20rpx;
align-items: center;
margin-bottom: 20rpx;
}
&__preview {
flex-shrink: 0;
width: 60rpx;
height: 60rpx;
border: 2rpx solid var(--wot-color-border);
border-radius: 8rpx;
}
&__input {
flex: 1;
}
&__tip {
font-size: 24rpx;
color: var(--wot-color-text-placeholder);
}
&__actions {
display: flex;
gap: 20rpx;
padding: 20rpx 30rpx 30rpx;
border-top: 1rpx solid var(--wot-color-border);
}
.popup-actions {
display: flex;
gap: 24rpx;
padding: 24rpx 32rpx;
border-top: 1rpx solid var(--color-border);
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<view class="page page--padding page--pt">
<view class="page page--padding">
<view>
<wd-search
v-model="queryParams.keywords"

View File

@@ -1,5 +1,5 @@
<template>
<view class="page page--padding page--pt">
<view class="page page--padding">
<view>
<wd-search
v-model="queryParams.keywords"

View File

@@ -1,5 +1,5 @@
<template>
<view class="page page--padding page--pt">
<view class="page page--padding">
<view>
<wd-search
v-model="queryParams.keywords"

View File

@@ -1,5 +1,5 @@
<template>
<view class="page page--padding page--pt">
<view class="page page--padding">
<view>
<wd-search
v-model="queryParams.keywords"

View File

@@ -1,5 +1,5 @@
<template>
<view class="work">
<view class="page page--padding page--tabbar">
<template v-for="(item, index) in visibleGridList" :key="index">
<wd-card :title="item.title">
<wd-grid clickable :column="4">
@@ -87,15 +87,3 @@ function handleNavClick(item: any) {
"layout": "tabbar"
}
</route>
<style lang="scss" scoped>
/* stylelint-disable selector-type-no-unknown */
page {
background: #f8f8f8;
}
/* stylelint-enable selector-type-no-unknown */
.work {
padding: 40rpx 0;
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<view class="page page--padding page--pt">
<view class="page page--padding">
<view>
<wd-search
v-model="queryParams.keywords"

View File

@@ -1,5 +1,5 @@
<template>
<view class="page page--padding page--pt">
<view class="page page--padding">
<view>
<wd-search
v-model="queryParams.keywords"
@@ -19,7 +19,7 @@
>
<!-- 自定义节点内容ID + 名称 + 状态 -->
<template #content="{ node }">
<view class="flex-1 flex flex-nowrap items-center gap-16rpx">
<view class="menu-node">
<text class="w-120rpx text-24rpx color-text-secondary">{{ node.id }}</text>
<wd-icon v-if="node.icon" :name="node.icon" size="16" class="color-primary" />
<text class="flex-1 truncate">{{ node.name }}</text>
@@ -401,4 +401,12 @@ export default { options: { styleIsolation: "shared" } };
gap: 24rpx;
margin-top: 32rpx;
}
.menu-node {
display: flex;
flex: 1;
flex-wrap: nowrap;
gap: 16rpx;
align-items: center;
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<view class="page page--padding page--pt">
<view class="page page--padding">
<view>
<wd-search
v-model="queryParams.title"

View File

@@ -1,5 +1,5 @@
<template>
<view class="page page--padding page--pt">
<view class="page page--padding">
<view>
<wd-search
v-model="queryParams.keywords"

View File

@@ -1,5 +1,5 @@
<template>
<view class="page page--padding page--pt">
<view class="page page--padding">
<view>
<wd-search
v-model="queryParams.keywords"
@@ -389,6 +389,12 @@ function showUserActions(item: UserPageVO) {
const actions: string[] = [];
const actionMap: Record<string, () => void> = {};
// 重置密码
if (hasPermission("sys:user:reset-password")) {
actions.push("重置密码");
actionMap["重置密码"] = () => handleResetPassword(item);
}
// 编辑
if (hasPermission("sys:user:update")) {
actions.push("编辑");
@@ -425,6 +431,28 @@ function showUserActions(item: UserPageVO) {
});
}
// 重置密码
async function handleResetPassword(item: UserPageVO) {
const { confirm, content: password } = await uni.showModal({
title: "重置密码",
editable: true,
placeholderText: `请输入用户「${item.username}」的新密码`,
});
if (!confirm || !password) return;
if (password.length < 6) {
toast.error("密码至少需要6位字符");
return;
}
try {
await UserAPI.resetPassword(item.id, password);
toast.success("密码重置成功");
} catch (error) {
// API 已处理错误提示
}
}
onReachBottom(() => {
if (queryParams.pageNum * queryParams.pageSize < total.value) {
fetchUserList();

View File

@@ -1,12 +1,21 @@
@use "./theme";
// ==========================================================================
// 全局基础样式
// 全局基础样式uni-app 跨端兼容)
// ==========================================================================
// 页面根元素,小程序和 H5 通用
page {
font-size: 28rpx;
line-height: 1.5;
color: var(--color-text);
background-color: var(--color-bg);
}
// H5 端字体平滑渲染
/* #ifdef H5 */
html,
body,
#app {
body {
height: 100%;
padding: 0;
margin: 0;
@@ -20,13 +29,10 @@ body,
"Helvetica Neue",
Helvetica,
sans-serif;
font-size: 28rpx;
line-height: 1.5;
color: var(--color-text);
background-color: var(--color-bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* #endif */
// ==========================================================================
// 页面容器
@@ -34,20 +40,18 @@ body,
.page {
min-height: 100vh;
background-color: var(--color-bg-page, var(--color-bg-secondary));
}
.page--auto {
min-height: auto;
background-color: var(--color-bg-secondary);
}
// 带内边距的页面
.page--padding {
padding: 0 32rpx;
box-sizing: border-box;
padding: 16rpx 32rpx 0;
}
.page--pt {
box-sizing: border-box;
padding-top: 16rpx;
// tabbar 页面底部安全区域
.page--tabbar {
padding-bottom: calc(100rpx + env(safe-area-inset-bottom));
}
// ==========================================================================
@@ -73,10 +77,11 @@ body,
}
// ==========================================================================
// 列表卡片
// 卡片组件
// ==========================================================================
.item-card.wd-card {
// 全局 wd-card 间距
.wd-card {
margin: 0 !important;
margin-bottom: 12rpx !important;
}
@@ -121,3 +126,25 @@ body,
color: #fff;
}
}
// ==========================================================================
// 渐变背景
// ==========================================================================
.gradient-bg {
&--primary {
background: linear-gradient(135deg, var(--color-primary-light) 0%, var(--color-bg) 100%);
}
&--success {
background: linear-gradient(135deg, var(--color-success-light) 0%, var(--color-bg) 100%);
}
&--warning {
background: linear-gradient(135deg, var(--color-warning-light) 0%, var(--color-bg) 100%);
}
&--danger {
background: linear-gradient(135deg, var(--color-danger-light) 0%, var(--color-bg) 100%);
}
}

View File

@@ -23,11 +23,10 @@ page {
--color-danger-light: #fff1f2;
// -----------------------------------------------------------------------
// 背景色
// 背景色 (Material Design)
// -----------------------------------------------------------------------
--color-bg: #ffffff;
--color-bg-secondary: #f8fafc;
--color-bg-page: #f1f5f9;
--color-bg-secondary: #f5f5f5;
// -----------------------------------------------------------------------
// 文字颜色
@@ -85,11 +84,10 @@ page {
--color-danger-light: #7f1d1d;
// -----------------------------------------------------------------------
// 背景色
// 背景色 (Material Design)
// -----------------------------------------------------------------------
--color-bg: #111827;
--color-bg-secondary: #1f2937;
--color-bg-page: #374151;
--color-bg: #121212;
--color-bg-secondary: #1e1e1e;
// -----------------------------------------------------------------------
// 文字颜色

3
src/types/env.d.ts vendored
View File

@@ -12,6 +12,9 @@ interface ImportMetaEnv {
/** API服务器URL */
readonly VITE_APP_API_URL: string;
/** 应用版本号 */
readonly VITE_APP_VERSION: string;
/**
* WebSocket 端点
*/

View File

@@ -4,6 +4,9 @@ import AutoImport from "unplugin-auto-import/vite";
import UniLayouts from "@uni-helper/vite-plugin-uni-layouts";
import UniPages from "@uni-helper/vite-plugin-uni-pages";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import Components from "@uni-helper/vite-plugin-uni-components";
import { WotResolver } from "@uni-helper/vite-plugin-uni-components/resolvers";
@@ -12,7 +15,14 @@ export default defineConfig(async ({ mode }: ConfigEnv): Promise<UserConfig> =>
const env = loadEnv(mode, process.cwd());
const isProd = mode === "production";
const pkg = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf-8")) as {
version?: string;
};
return {
define: {
"import.meta.env.VITE_APP_VERSION": JSON.stringify(pkg.version ?? ""),
},
server: {
host: "0.0.0.0",
port: +env.VITE_APP_PORT,