fix: 页面编码问题修复
This commit is contained in:
@@ -42,7 +42,11 @@
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/account/index",
|
||||
"type": "page"
|
||||
"type": "page",
|
||||
"style": {
|
||||
"navigationBarTitleText": "账号安全",
|
||||
"backgroundColor": "#f1f5f9"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/feedback/index",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<view class="home-page">
|
||||
<!-- 頧格偘<EFBFBD>?-->
|
||||
<!-- 轮播图 -->
|
||||
<view class="hero">
|
||||
<wd-swiper
|
||||
v-model:current="current"
|
||||
@@ -30,19 +30,13 @@
|
||||
</view>
|
||||
|
||||
<!-- 通知公告 -->
|
||||
<view v-if="noticeList.length" class="section">
|
||||
<view class="section">
|
||||
<view class="notice-bar" @click="handleNoticeClick">
|
||||
<view class="notice-bar__icon">
|
||||
<wd-icon name="check-outline" size="32rpx" color="#34D19D" />
|
||||
</view>
|
||||
<view class="notice-bar__content">
|
||||
<view v-if="isNoticeScrolling" class="notice-bar__marquee">
|
||||
<view class="notice-bar__marquee-inner">
|
||||
<text class="notice-bar__text">{{ noticeText }}</text>
|
||||
<text class="notice-bar__text notice-bar__text--gap">{{ noticeText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text v-else class="notice-bar__text">{{ noticeText }}</text>
|
||||
<text class="notice-bar__text">{{ noticeText || "暂无通知公告" }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -55,7 +49,7 @@
|
||||
<view class="stat-card__icon stat-card__icon--user"></view>
|
||||
</view>
|
||||
<view class="stat-card__head">
|
||||
<text class="stat-card__label">霈踹恥<EFBFBD>?/text>
|
||||
<text class="stat-card__label">访客数</text>
|
||||
<view class="stat-card__dot stat-card__dot--green"></view>
|
||||
</view>
|
||||
<text class="stat-card__num stat-card__num--green">
|
||||
@@ -67,7 +61,7 @@
|
||||
<view class="stat-card__icon stat-card__icon--eye"></view>
|
||||
</view>
|
||||
<view class="stat-card__head">
|
||||
<text class="stat-card__label">瘚讛<EFBFBD><EFBFBD>?/text>
|
||||
<text class="stat-card__label">浏览量</text>
|
||||
<view class="stat-card__dot stat-card__dot--blue"></view>
|
||||
</view>
|
||||
<text class="stat-card__num stat-card__num--blue">
|
||||
@@ -99,8 +93,8 @@
|
||||
inline
|
||||
@change="handleDataRangeChange"
|
||||
>
|
||||
<wd-radio :value="7">餈?憭?/wd-radio>
|
||||
<wd-radio :value="15">餈?5憭?/wd-radio>
|
||||
<wd-radio :value="7">近7天</wd-radio>
|
||||
<wd-radio :value="15">近15天</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
</template>
|
||||
@@ -117,15 +111,19 @@
|
||||
/**
|
||||
* 首页
|
||||
*
|
||||
* <EFBFBD>蠘<EFBFBD>霂湔<EFBFBD>嚗? * - <20>芸<EFBFBD>銋匧紡<E58CA7>芣<EFBFBD>嚗<EFBFBD><E59A97>蝝X<E89D9D>嚗? * - 頧格偘<E6A0BC>曉<EFBFBD>蝷? * - 敹急㭘撖潸⏛<E6BDB8>亙藁
|
||||
* 功能说明:
|
||||
* - 自定义导航栏(搜索框)
|
||||
* - 轮播图展示
|
||||
* - 快捷导航入口
|
||||
* - 通知公告
|
||||
* - 数据统计
|
||||
* - 访问趋势图表
|
||||
*
|
||||
* 技术要点:
|
||||
* - 雿輻鍂 useNavbar 憭<EFBFBD><EFBFBD><EFBFBD>芸<EFBFBD>銋匧紡<EFBFBD>芣<EFBFBD>擃睃漲<EFBFBD>諹<EFBFBD><EFBFBD>𢠃<EFBFBD>霈?
|
||||
* - 使用 useNavbar 处理自定义导航栏高度和胶囊避让;
|
||||
* - 使用 BEM 命名规范组织 CSS
|
||||
* - <EFBFBD>舀<EFBFBD><EFBFBD>烾<EFBFBD>璅∪<EFBFBD>嚗<EFBFBD><EFBFBD>朞<EFBFBD> CSS <20>㗛<EFBFBD>嚗? */
|
||||
* - 支持暗黑模式(通过 CSS 变量)
|
||||
*/
|
||||
|
||||
import { computed, ref } from "vue";
|
||||
import { onReady } from "@dcloudio/uni-app";
|
||||
@@ -156,9 +154,11 @@ interface NavItem {
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
// custom-navbar 蝏<EFBFBD>辣<EFBFBD><EFBFBD><EFBFBD>撌脣<EFBFBD><EFBFBD><EFBFBD>紡<EFBFBD>芣<EFBFBD>擃睃漲銝舘<EFBFBD><EFBFBD>𢠃<EFBFBD>霈?
|
||||
// custom-navbar 组件内部已处理导航栏高度与胶囊避让
|
||||
|
||||
// ============================================================================
|
||||
// 响应式数据
|
||||
// ============================================================================
|
||||
// <20>滚<EFBFBD>撘𤩺㺭<F0A4A9BA>?// ============================================================================
|
||||
|
||||
const current = ref(0);
|
||||
const recentDaysRange = ref(7);
|
||||
@@ -178,19 +178,23 @@ const appVersion = ref<string>("");
|
||||
|
||||
const noticeList = ref<NoticePageVO[]>([]);
|
||||
const noticeText = computed(() => {
|
||||
if (!noticeList.value.length) {
|
||||
return "暂无通知";
|
||||
}
|
||||
const titles = noticeList.value
|
||||
.map((n: NoticePageVO) => n.title)
|
||||
.filter(Boolean)
|
||||
.slice(0, 5) as string[];
|
||||
return titles.length ? titles.join(" ") : "";
|
||||
.slice(0, 2) as string[];
|
||||
return titles.length ? titles.join(" ") : "暂无通知";
|
||||
});
|
||||
|
||||
const isNoticeScrolling = computed(() => noticeList.value.length > 1);
|
||||
|
||||
// 用户权限列表
|
||||
const userPerms = computed(() => userStore.userInfo?.perms || []);
|
||||
|
||||
// <EFBFBD>臬炏撌脩蒈敶?const isLogged = computed(() => isLoggedIn());
|
||||
// 是否已登录
|
||||
const isLogged = computed(() => isLoggedIn());
|
||||
|
||||
const hasAnyPerm = computed(() => userPerms.value.length > 0);
|
||||
|
||||
// 检查是否有权限
|
||||
const hasPermission = (perm: string) => {
|
||||
@@ -212,8 +216,9 @@ const defaultNavList = computed(() => {
|
||||
return result;
|
||||
});
|
||||
|
||||
// 敹急㭘<EFBFBD>亙藁嚗𡁜歇<EFBFBD>餃<EFBFBD><EFBFBD>㗇<EFBFBD><EFBFBD>鞱<EFBFBD>皛歹<EFBFBD><EFBFBD>芰蒈敶閙遬蝷粹<EFBFBD>霈方<EFBFBD><EFBFBD>?const quickNavList = computed(() => {
|
||||
if (!isLogged.value) {
|
||||
// 快捷入口:已登录按权限过滤,未登录显示默认菜单
|
||||
const quickNavList = computed(() => {
|
||||
if (!isLogged.value || !hasAnyPerm.value) {
|
||||
return defaultNavList.value;
|
||||
}
|
||||
const result: { icon: string; title: string; url: string; perm: string }[] = [];
|
||||
@@ -259,6 +264,7 @@ const chartOpts = ref({
|
||||
|
||||
function loadAppVersion() {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const p: any = (globalThis as any).plus;
|
||||
if (p?.runtime?.version) {
|
||||
appVersion.value = `v${p.runtime.version}`;
|
||||
@@ -275,7 +281,7 @@ async function loadNoticeData() {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { list } = await NoticeAPI.getMyNoticePage({ pageNum: 1, pageSize: 5 });
|
||||
const { list } = await NoticeAPI.getMyNoticePage({ pageNum: 1, pageSize: 2 });
|
||||
noticeList.value = list || [];
|
||||
} catch {
|
||||
noticeList.value = [];
|
||||
@@ -292,9 +298,7 @@ async function loadVisitStatsData() {
|
||||
|
||||
async function loadVisitTrendData() {
|
||||
const endDate = dayjs().format("YYYY-MM-DD");
|
||||
const startDate = dayjs()
|
||||
.subtract(recentDaysRange.value - 1, "day")
|
||||
.format("YYYY-MM-DD");
|
||||
const startDate = dayjs().subtract(recentDaysRange.value - 1, "day").format("YYYY-MM-DD");
|
||||
|
||||
try {
|
||||
const data: VisitTrendVO = await LogAPI.getVisitTrend({ startDate, endDate });
|
||||
@@ -302,8 +306,8 @@ async function loadVisitTrendData() {
|
||||
JSON.stringify({
|
||||
categories: (data.dates || []).map((d) => dayjs(d).format("MM-DD")),
|
||||
series: [
|
||||
{ name: "霈踹恥<EFBFBD>?UV)", data: data.uvList || [] },
|
||||
{ name: "瘚讛<EFBFBD><EFBFBD>?PV)", data: data.pvList || [] },
|
||||
{ name: "访客数(UV)", data: data.uvList || [] },
|
||||
{ name: "浏览量(PV)", data: data.pvList || [] },
|
||||
],
|
||||
})
|
||||
);
|
||||
@@ -317,9 +321,14 @@ async function loadVisitTrendData() {
|
||||
// ============================================================================
|
||||
|
||||
function handleNavClick(item: NavItem) {
|
||||
// <EFBFBD>芰蒈敶閙𧒄<EFBFBD><EFBFBD><EFBFBD><EFBFBD>亦蒈敶閧𠶖<EFBFBD><EFBFBD>僎頝唾蓮<EFBFBD>餃<EFBFBD>憿? if (!checkLogin()) {
|
||||
// 未登录 / 无权限时:展示默认导航,但点击统一跳登录
|
||||
if (!isLogged.value || !hasAnyPerm.value) {
|
||||
uni.navigateTo({ url: "/pages/login/index" });
|
||||
return;
|
||||
}
|
||||
// 已登录但访问受限时,仍做一次登录校验(防 token 过期)
|
||||
if (!checkLogin()) return;
|
||||
|
||||
router.push({ path: item.url });
|
||||
}
|
||||
|
||||
@@ -373,28 +382,29 @@ onReady(() => {
|
||||
.hero {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hero::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
height: 120rpx;
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(245, 247, 250, 0) 0%,
|
||||
rgba(245, 247, 250, 0.85) 60%,
|
||||
rgba(245, 247, 250, 1) 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
:deep(.swiper-box) {
|
||||
height: 420rpx;
|
||||
overflow: hidden;
|
||||
border-bottom-right-radius: 32rpx;
|
||||
border-bottom-left-radius: 32rpx;
|
||||
border-bottom-right-radius: 32rpx;
|
||||
}
|
||||
|
||||
:deep(.swiper-box .wd-swiper__item),
|
||||
@@ -403,7 +413,8 @@ onReady(() => {
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 撖潸⏛<EFBFBD>?// ============================================================================
|
||||
// 导航栏
|
||||
// ============================================================================
|
||||
|
||||
.navbar {
|
||||
position: fixed;
|
||||
@@ -456,15 +467,16 @@ onReady(() => {
|
||||
.section--overlay {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding: 18rpx 8rpx;
|
||||
margin-top: -140rpx;
|
||||
padding: 18rpx 8rpx;
|
||||
background: var(--color-bg);
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 16rpx 36rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 敹急㭘撖潸⏛憿?// ============================================================================
|
||||
// 快捷导航项
|
||||
// ============================================================================
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
@@ -486,27 +498,28 @@ onReady(() => {
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// <EFBFBD>𡁶䰻<EFBFBD>?// ============================================================================
|
||||
// 通知栏
|
||||
// ============================================================================
|
||||
|
||||
.notice-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx 24rpx 24rpx 20rpx;
|
||||
background: #fff;
|
||||
border: 1rpx solid rgba(0, 0, 0, 0.04);
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx 24rpx 24rpx 20rpx;
|
||||
border: 1rpx solid rgba(0, 0, 0, 0.04);
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
|
||||
|
||||
&__icon {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
margin-right: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(52, 209, 157, 0.1);
|
||||
border-radius: 12rpx;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
&__content {
|
||||
@@ -516,35 +529,14 @@ onReady(() => {
|
||||
|
||||
&__text {
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__text--gap {
|
||||
padding-left: 48rpx;
|
||||
}
|
||||
|
||||
&__marquee {
|
||||
width: 100%;
|
||||
font-weight: 500;
|
||||
display: -webkit-box;
|
||||
line-clamp: 1;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__marquee-inner {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
animation: notice-marquee 14s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes notice-marquee {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -558,49 +550,55 @@ onReady(() => {
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 24rpx 20rpx;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
border: 1rpx solid rgba(0, 0, 0, 0.04);
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
|
||||
border: 1rpx solid rgba(0, 0, 0, 0.04);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
// 背景图标容器
|
||||
&__bg {
|
||||
position: absolute;
|
||||
right: 12rpx;
|
||||
bottom: 12rpx;
|
||||
width: 70rpx;
|
||||
height: 70rpx;
|
||||
pointer-events: none;
|
||||
opacity: 0.08;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// SVG 装饰图形
|
||||
&__icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
// 用户组图标 - 访客数
|
||||
&--user {
|
||||
background-image: url("/static/icons/visitor.svg");
|
||||
background-repeat: no-repeat;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48' fill='%232ab789'%3E%3Ccircle cx='24' cy='16' r='8'/%3E%3Ccircle cx='10' cy='20' r='6'/%3E%3Ccircle cx='38' cy='20' r='6'/%3E%3Cpath d='M24 28c-8 0-14 4-14 8v6h28v-6c0-4-6-8-14-8z'/%3E%3Cpath d='M10 28c-4 0-8 2-8 5v5h8v-5c0-2 1-4 3-5-1 0-2 0-3 0z'/%3E%3Cpath d='M38 28c4 0 8 2 8 5v5h-8v-5c0-2-1-4-3-5 1 0 2 0 3 0z'/%3E%3C/svg%3E");
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
// 图表图标 - 浏览量
|
||||
&--eye {
|
||||
background-image: url("/static/icons/browser.svg");
|
||||
background-repeat: no-repeat;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48' fill='%233a8ee6'%3E%3Crect x='4' y='24' width='8' height='20' rx='2'/%3E%3Crect x='16' y='14' width='8' height='30' rx='2'/%3E%3Crect x='28' y='20' width='8' height='24' rx='2'/%3E%3Crect x='40' y='8' width='8' height='36' rx='2'/%3E%3C/svg%3E");
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
// 盾牌徽章图标 - 版本
|
||||
&--gear {
|
||||
background-image: url("/static/icons/setting.svg");
|
||||
background-repeat: no-repeat;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48' fill='%23e8a838'%3E%3Cpath d='M24 4L8 10v12c0 11 8 18 16 22 8-4 16-11 16-22V10L24 4z'/%3E%3Cpath d='M20 24l4 4 8-8' stroke='white' stroke-width='3' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
// 彩色卡片变体
|
||||
&--green {
|
||||
background: linear-gradient(135deg, #f0fdf9 0%, #fff 100%);
|
||||
}
|
||||
@@ -614,18 +612,18 @@ onReady(() => {
|
||||
}
|
||||
|
||||
&__head {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
color: #999;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__dot {
|
||||
@@ -650,12 +648,12 @@ onReady(() => {
|
||||
}
|
||||
|
||||
&__num {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
letter-spacing: -1rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
&--green {
|
||||
color: #2ab789;
|
||||
@@ -674,8 +672,8 @@ onReady(() => {
|
||||
}
|
||||
|
||||
&--full {
|
||||
flex-direction: row;
|
||||
grid-column: 1 / -1;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 20rpx;
|
||||
@@ -690,10 +688,10 @@ onReady(() => {
|
||||
}
|
||||
|
||||
.stat-card__bg {
|
||||
right: 16rpx;
|
||||
bottom: 50%;
|
||||
width: 50rpx;
|
||||
height: 50rpx;
|
||||
right: 16rpx;
|
||||
bottom: 50%;
|
||||
transform: translateY(50%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,14 @@
|
||||
<view class="login-nav" :style="{ paddingTop: `${statusBarHeight}px` }">
|
||||
<view class="login-nav__bar" :style="{ height: `${navBarHeight}px` }">
|
||||
<view class="login-nav__action" hover-class="login-nav__action--active" @click="handleBack">
|
||||
<text class="login-nav__back-icon"><EFBFBD>?/text>
|
||||
<text class="login-nav__back-icon">‹</text>
|
||||
</view>
|
||||
<view class="login-nav__title" />
|
||||
<view class="login-nav__placeholder" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 銝餃<EFBFBD>摰?-->
|
||||
<!-- 主内容 -->
|
||||
<view class="login-main" :style="{ paddingTop: `${statusBarHeight + navBarHeight + 4}px` }">
|
||||
<!-- Logo -->
|
||||
<view class="login-logo">
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view v-if="loginMode !== 'WECHAT'" class="form-area">
|
||||
<!-- <EFBFBD>冽<EFBFBD><EFBFBD>?<EFBFBD>𧢲㦤<EFBFBD>?-->
|
||||
<!-- 用户名/手机号 -->
|
||||
<view class="input-box">
|
||||
<wd-icon name="person" size="20" color="#9ca3af" />
|
||||
<input
|
||||
@@ -52,7 +52,7 @@
|
||||
v-model="formData.password"
|
||||
class="input-field"
|
||||
:password="!showPassword"
|
||||
placeholder="霂瑁<EFBFBD><EFBFBD>亙<EFBFBD><EFBFBD>?
|
||||
placeholder="请输入密码"
|
||||
:maxlength="50"
|
||||
@confirm="handleLogin"
|
||||
/>
|
||||
@@ -80,7 +80,7 @@
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 撉諹<EFBFBD><EFBFBD>?-->
|
||||
<!-- 验证码 -->
|
||||
<view v-else class="input-box">
|
||||
<wd-icon name="shield" size="20" color="#9ca3af" />
|
||||
<input
|
||||
@@ -96,7 +96,7 @@
|
||||
:class="smsCountdown > 0 ? 'sms-btn-disabled' : 'sms-btn-active'"
|
||||
@click="handleSendCode"
|
||||
>
|
||||
{{ smsCountdown > 0 ? `${smsCountdown}s` : "<EFBFBD>瑕<EFBFBD>撉諹<EFBFBD><EFBFBD>? }}
|
||||
{{ smsCountdown > 0 ? `${smsCountdown}s` : "获取验证码" }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -112,16 +112,16 @@
|
||||
:disabled="loading"
|
||||
@click="handleLogin"
|
||||
>
|
||||
{{ loading ? "<EFBFBD>餃<EFBFBD>銝?.." : "<EFBFBD>?敶? }}
|
||||
{{ loading ? "登录中..." : "登 录" }}
|
||||
</button>
|
||||
|
||||
<!-- 切换登录方式 -->
|
||||
<view class="switch-mode" @click="toggleLoginMode">
|
||||
<text class="switch-text">
|
||||
{{ loginMode === "PASSWORD" ? "敹䁅扇撖<EFBFBD><EFBFBD>嚗? : "霈啣<EFBFBD>撖<EFBFBD><EFBFBD>嚗? }}
|
||||
{{ loginMode === "PASSWORD" ? "忘记密码?" : "记得密码?" }}
|
||||
</text>
|
||||
<text class="switch-link">
|
||||
{{ loginMode === "PASSWORD" ? "撉諹<EFBFBD><EFBFBD><EFBFBD>蒈敶? : "撖<EFBFBD><EFBFBD><EFBFBD>餃<EFBFBD>" }}
|
||||
{{ loginMode === "PASSWORD" ? "验证码登录" : "密码登录" }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -135,7 +135,8 @@
|
||||
@getphonenumber="handleWechatPhoneLogin"
|
||||
>
|
||||
<image class="btn-wechat-icon" src="/static/icons/weixin.png" mode="aspectFit" />
|
||||
敺桐縑銝<EFBFBD><EFBFBD>桃蒈敶? </button>
|
||||
微信一键登录
|
||||
</button>
|
||||
|
||||
<!-- 其他登录方式 -->
|
||||
<view class="switch-mode" @click="loginMode = 'PASSWORD'">
|
||||
@@ -146,7 +147,7 @@
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<!-- <20><>𠧧蝥?-->
|
||||
<!-- 分割线 -->
|
||||
<view v-if="loginMode !== 'WECHAT'" class="divider">
|
||||
<view class="divider-line" />
|
||||
<text class="divider-text">其他登录方式</text>
|
||||
@@ -166,10 +167,14 @@
|
||||
<view class="agreement">
|
||||
<wd-checkbox v-model="isAgreePolicy" shape="square" size="16px">
|
||||
<text class="agreement-text">
|
||||
<EFBFBD>穃歇<EFBFBD><EFBFBD>粉撟嗅<EFBFBD><EFBFBD>? <text class="agreement-link" @click.stop="navigateToAgreement('user')">
|
||||
<20>羓鍂<E7BE93>瑕<EFBFBD>霈柴<E99C88>? </text>
|
||||
銝? <text class="agreement-link" @click.stop="navigateToAgreement('privacy')">
|
||||
<20>𢠃<EFBFBD>蝘<EFBFBD>錇蝑硔<E89D91>? </text>
|
||||
我已阅读并同意
|
||||
<text class="agreement-link" @click.stop="navigateToAgreement('user')">
|
||||
《用户协议》
|
||||
</text>
|
||||
与
|
||||
<text class="agreement-link" @click.stop="navigateToAgreement('privacy')">
|
||||
《隐私政策》
|
||||
</text>
|
||||
</text>
|
||||
</wd-checkbox>
|
||||
</view>
|
||||
@@ -177,7 +182,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 蝏穃<E89D8F><E7A983>𧢲㦤<F0A7A2B2>瑕撕蝒?-->
|
||||
<!-- 绑定手机号弹窗 -->
|
||||
<wd-popup
|
||||
v-model="showBindMobilePopup"
|
||||
position="bottom"
|
||||
@@ -186,7 +191,7 @@
|
||||
@close="resetBindForm"
|
||||
>
|
||||
<view class="bind-popup">
|
||||
<text class="bind-title">蝏穃<E89D8F><E7A983>𧢲㦤<F0A7A2B2>?/text>
|
||||
<text class="bind-title">绑定手机号</text>
|
||||
|
||||
<view class="form-area">
|
||||
<view class="input-box">
|
||||
@@ -214,7 +219,7 @@
|
||||
:class="bindSmsCountdown > 0 ? 'sms-btn-disabled' : 'sms-btn-active'"
|
||||
@click="handleSendBindCode"
|
||||
>
|
||||
{{ bindSmsCountdown > 0 ? `${bindSmsCountdown}s` : "<EFBFBD>瑕<EFBFBD>撉諹<EFBFBD><EFBFBD>? }}
|
||||
{{ bindSmsCountdown > 0 ? `${bindSmsCountdown}s` : "获取验证码" }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -224,7 +229,7 @@
|
||||
</view>
|
||||
|
||||
<button class="btn-bind" :disabled="bindLoading" @click="handleBindMobile">
|
||||
{{ bindLoading ? "蝏穃<EFBFBD>銝?.." : "蝖株恕蝏穃<EFBFBD>" }}
|
||||
{{ bindLoading ? "绑定中..." : "确认绑定" }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
@@ -233,8 +238,9 @@
|
||||
<!-- 协议确认弹窗 -->
|
||||
<wd-message-box selector="policy-box">
|
||||
<view class="text-center text-sm text-gray-500 leading-relaxed">
|
||||
霂琿<EFBFBD>霂餃僎<EFBFBD>峕<EFBFBD><EFBFBD>㗇䔉<EFBFBD><EFBFBD><EFBFBD>? <text class="text-blue-500" @click.stop="navigateToAgreement('user')"><EFBFBD>羓鍂<EFBFBD>瑕<EFBFBD>霈柴<EFBFBD>?/text>
|
||||
<text class="text-blue-500" @click.stop="navigateToAgreement('privacy')"><EFBFBD>𢠃<EFBFBD>蝘<EFBFBD>錇蝑硔<EFBFBD>?/text>
|
||||
请阅读并同意有来技术
|
||||
<text class="text-blue-500" @click.stop="navigateToAgreement('user')">《用户协议》</text>
|
||||
<text class="text-blue-500" @click.stop="navigateToAgreement('privacy')">《隐私政策》</text>
|
||||
</view>
|
||||
</wd-message-box>
|
||||
|
||||
@@ -262,7 +268,8 @@ const toast = useToast();
|
||||
const message = useMessage("policy-box");
|
||||
const userStore = useUserStore();
|
||||
|
||||
// <EFBFBD>嗆<EFBFBD>?const loading = ref(false);
|
||||
// 状态
|
||||
const loading = ref(false);
|
||||
const showPassword = ref(false);
|
||||
const isAgreePolicy = ref(false);
|
||||
const loginMode = ref<"PASSWORD" | "SMS" | "WECHAT">("PASSWORD");
|
||||
@@ -276,13 +283,15 @@ const formData = ref({
|
||||
captchaCode: "",
|
||||
});
|
||||
|
||||
// <EFBFBD>曉耦撉諹<EFBFBD><EFBFBD>?const captchaId = ref("");
|
||||
// 图形验证码
|
||||
const captchaId = ref("");
|
||||
const captchaBase64 = ref("");
|
||||
const captchaLoading = ref(false);
|
||||
|
||||
const redirect = ref("/pages/index/index");
|
||||
|
||||
// 蝏穃<EFBFBD><EFBFBD>𧢲㦤<EFBFBD>瑞㮾<EFBFBD>?const showBindMobilePopup = ref(false);
|
||||
// 绑定手机号相关
|
||||
const showBindMobilePopup = ref(false);
|
||||
const bindLoading = ref(false);
|
||||
const bindSmsCountdown = ref(0);
|
||||
const bindSmsTimer = ref<ReturnType<typeof setInterval> | null>(null);
|
||||
@@ -296,7 +305,8 @@ const bindMobileForm = ref({
|
||||
const pendingLoginAction = ref<"FORM" | "WECHAT_PHONE" | null>(null);
|
||||
const pendingWechatPhoneCode = ref<string>("");
|
||||
|
||||
// <EFBFBD>瑕<EFBFBD><EFBFBD>曉耦撉諹<EFBFBD><EFBFBD>?const fetchCaptcha = async () => {
|
||||
// 获取图形验证码
|
||||
const fetchCaptcha = async () => {
|
||||
if (captchaLoading.value) return;
|
||||
try {
|
||||
captchaLoading.value = true;
|
||||
@@ -305,16 +315,17 @@ const pendingWechatPhoneCode = ref<string>("");
|
||||
captchaId.value = res.captchaId;
|
||||
captchaBase64.value = res.captchaBase64;
|
||||
} catch (e) {
|
||||
console.error("<EFBFBD>瑕<EFBFBD>撉諹<EFBFBD><EFBFBD><EFBFBD>仃韐?, e);
|
||||
console.error("获取验证码失败", e);
|
||||
} finally {
|
||||
captchaLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 霈∠<EFBFBD>撅墧<EFBFBD>?const loginModeDesc = computed(() => {
|
||||
// 计算属性
|
||||
const loginModeDesc = computed(() => {
|
||||
const modeMap = {
|
||||
PASSWORD: "使用账号密码登录",
|
||||
SMS: "雿輻鍂<EFBFBD>𧢲㦤撉諹<EFBFBD><EFBFBD><EFBFBD>蒈敶?,
|
||||
SMS: "使用手机验证码登录",
|
||||
WECHAT: "使用微信快捷登录",
|
||||
};
|
||||
return modeMap[loginMode.value];
|
||||
@@ -351,11 +362,13 @@ onLoad((options: any) => {
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
// <EFBFBD>𧼮凝靽∠㴓憓<EFBFBD>撩<EFBFBD>嗡蝙<EFBFBD>典<EFBFBD><EFBFBD><EFBFBD>蒈敶? if (loginMode.value === "WECHAT") {
|
||||
// 非微信环境强制使用密码登录
|
||||
if (loginMode.value === "WECHAT") {
|
||||
loginMode.value = "PASSWORD";
|
||||
}
|
||||
// #endif
|
||||
// <EFBFBD>瑕<EFBFBD><EFBFBD>曉耦撉諹<EFBFBD><EFBFBD>? fetchCaptcha();
|
||||
// 获取图形验证码
|
||||
fetchCaptcha();
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
@@ -412,9 +425,9 @@ const openPolicyDialog = (action: "FORM" | "WECHAT_PHONE", phoneCode: string = "
|
||||
async function doFormLogin() {
|
||||
if (!canSubmit.value) {
|
||||
if (loginMode.value === "PASSWORD") {
|
||||
toast.error("霂瑁<EFBFBD><EFBFBD>亦鍂<EFBFBD>瑕<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>?);
|
||||
toast.error("请输入用户名和密码");
|
||||
} else if (loginMode.value === "SMS") {
|
||||
toast.error("霂瑁<EFBFBD><EFBFBD>交迤蝖桃<EFBFBD><EFBFBD>𧢲㦤<EFBFBD>瑕<EFBFBD>撉諹<EFBFBD><EFBFBD>?);
|
||||
toast.error("请输入正确的手机号和验证码");
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -443,7 +456,8 @@ async function doFormLogin() {
|
||||
setTimeout(() => uni.reLaunch({ url: redirect.value }), 800);
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || "登录失败");
|
||||
// <EFBFBD>餃<EFBFBD>憭梯揖<EFBFBD>瑟鰵撉諹<EFBFBD><EFBFBD>? if (loginMode.value === "PASSWORD") {
|
||||
// 登录失败刷新验证码
|
||||
if (loginMode.value === "PASSWORD") {
|
||||
fetchCaptcha();
|
||||
}
|
||||
} finally {
|
||||
@@ -467,16 +481,16 @@ const handleSendCode = async () => {
|
||||
return;
|
||||
}
|
||||
if (!isValidMobile(mobile)) {
|
||||
toast.error("霂瑁<EFBFBD><EFBFBD>交迤蝖桃<EFBFBD><EFBFBD>𧢲㦤<EFBFBD>?);
|
||||
toast.error("请输入正确的手机号");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await AuthAPI.sendSmsLoginCode(mobile);
|
||||
toast.success("撉諹<EFBFBD><EFBFBD><EFBFBD>歇<EFBFBD>煾<EFBFBD>?);
|
||||
toast.success("验证码已发送");
|
||||
startSmsCountdown(smsCountdown, smsTimer);
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || "<EFBFBD>煾<EFBFBD><EFBFBD>仃韐?);
|
||||
toast.error(error?.message || "发送失败");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -560,16 +574,16 @@ const handleSendBindCode = async () => {
|
||||
if (bindSmsCountdown.value > 0) return;
|
||||
const mobile = bindMobileForm.value.mobile.trim();
|
||||
if (!isValidMobile(mobile)) {
|
||||
toast.error("霂瑁<EFBFBD><EFBFBD>交迤蝖桃<EFBFBD><EFBFBD>𧢲㦤<EFBFBD>?);
|
||||
toast.error("请输入正确的手机号");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await AuthAPI.sendSmsLoginCode(mobile);
|
||||
toast.success("撉諹<EFBFBD><EFBFBD><EFBFBD>歇<EFBFBD>煾<EFBFBD>?);
|
||||
toast.success("验证码已发送");
|
||||
startSmsCountdown(bindSmsCountdown, bindSmsTimer);
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || "<EFBFBD>煾<EFBFBD><EFBFBD>仃韐?);
|
||||
toast.error(error?.message || "发送失败");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -586,7 +600,7 @@ const handleBindMobile = async () => {
|
||||
if (bindLoading.value) return;
|
||||
const { mobile, code } = bindMobileForm.value;
|
||||
if (!isValidMobile(mobile)) {
|
||||
toast.error("霂瑁<EFBFBD><EFBFBD>交迤蝖桃<EFBFBD><EFBFBD>𧢲㦤<EFBFBD>?);
|
||||
toast.error("请输入正确的手机号");
|
||||
return;
|
||||
}
|
||||
if (!code.trim()) {
|
||||
@@ -732,7 +746,8 @@ const handleBack = () => {
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
// 銝餃<EFBFBD>摰?.login-main {
|
||||
// 主内容
|
||||
.login-main {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
@@ -819,7 +834,8 @@ const handleBack = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 颲枏<EFBFBD>獢<EFBFBD>捆<EFBFBD>?.input-box {
|
||||
// 输入框容器
|
||||
.input-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 48px;
|
||||
@@ -852,7 +868,8 @@ const handleBack = () => {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
// 銝餅<EFBFBD><EFBFBD>?.btn-primary {
|
||||
// 主按钮
|
||||
.btn-primary {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -950,7 +967,8 @@ const handleBack = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 撉諹<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>?.sms-btn {
|
||||
// 验证码按钮
|
||||
.sms-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -1017,7 +1035,8 @@ const handleBack = () => {
|
||||
border-bottom-color: #60a5fa;
|
||||
}
|
||||
|
||||
// <EFBFBD><EFBFBD>𠧧蝥?.divider {
|
||||
// 分割线
|
||||
.divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 24px;
|
||||
@@ -1075,7 +1094,8 @@ const handleBack = () => {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
// 蝏穃<EFBFBD><EFBFBD>𧢲㦤<EFBFBD>瑕撕蝒?.bind-popup {
|
||||
// 绑定手机号弹窗
|
||||
.bind-popup {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +1,40 @@
|
||||
<template>
|
||||
<view class="page dark:text-[var(--wot-color-text)]">
|
||||
<view class="page page--full account-page dark:text-[var(--wot-color-text)] py-2">
|
||||
<wd-card>
|
||||
<wd-cell-group border>
|
||||
<wd-cell
|
||||
icon="secured"
|
||||
title="账户密码"
|
||||
label="定期修改密码有助于保护账户安全"
|
||||
value="修改"
|
||||
is-link
|
||||
@click="handleOpenDialog(DialogType.PASSWORD)"
|
||||
/>
|
||||
<wd-cell
|
||||
icon="mobile"
|
||||
title="绑定手机"
|
||||
:value="userProfile?.mobile"
|
||||
:value="userProfile?.mobile || '未绑定手机号'"
|
||||
is-link
|
||||
@click="handleOpenDialog(DialogType.MOBILE)"
|
||||
/>
|
||||
<wd-cell
|
||||
icon="mail"
|
||||
title="绑定邮箱"
|
||||
:value="userProfile?.email ? userProfile.email : '未绑定邮箱'"
|
||||
is-link
|
||||
@click="handleOpenDialog(DialogType.EMAIL)"
|
||||
/>
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<wd-cell title="微信" value="解绑" is-link @click="handleUnbindWechat" />
|
||||
<wd-cell icon="wechat" title="微信" value="解绑" is-link @click="handleUnbindWechat" />
|
||||
<!-- #endif -->
|
||||
</wd-cell-group>
|
||||
</wd-card>
|
||||
|
||||
<!--用户信息编辑弹出框-->
|
||||
<wd-popup v-model="dialog.visible" position="bottom">
|
||||
<wd-popup
|
||||
v-model="dialog.visible"
|
||||
position="bottom"
|
||||
custom-style="border-top-left-radius: 24rpx; border-top-right-radius: 24rpx;"
|
||||
>
|
||||
<wd-form
|
||||
v-if="dialog.type === DialogType.PASSWORD"
|
||||
ref="passwordChangeFormRef"
|
||||
@@ -155,6 +161,7 @@
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, reactive, ref } from "vue";
|
||||
import UserAPI, {
|
||||
PasswordChangeForm,
|
||||
MobileBindingForm,
|
||||
@@ -340,5 +347,17 @@ function handleSubmit() {
|
||||
onMounted(() => {
|
||||
loadUserProfile();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (mobileTimer.value) {
|
||||
clearInterval(mobileTimer.value);
|
||||
mobileTimer.value = null;
|
||||
}
|
||||
if (emailTimer.value) {
|
||||
clearInterval(emailTimer.value as unknown as number);
|
||||
emailTimer.value = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<view class="page dark:text-[var(--wot-color-text)]">
|
||||
<view class="page-container dark:text-[var(--wot-color-text)]">
|
||||
<wd-text size="small">选填,最多上传3张图片</wd-text>
|
||||
<wd-form ref="formRef" :model="formData" :rules="rules">
|
||||
<wd-form-item label="问题类型" prop="feedbackType">
|
||||
|
||||
@@ -198,7 +198,7 @@ const userInfo = computed(() => userStore.userInfo);
|
||||
const defaultAvatar = "/static/images/default-avatar.png";
|
||||
|
||||
const hasAccessToken = ref(!!getAccessToken());
|
||||
const isLogin = computed(() => hasAccessToken.value);
|
||||
const isLogin = computed(() => !!getAccessToken());
|
||||
|
||||
const headerBackground = computed(() => {
|
||||
const color = currentThemeColor.value || "#4d80f0";
|
||||
@@ -517,8 +517,8 @@ const helpItems = computed<ActionItem[]>(() => [
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 320rpx;
|
||||
left: 0;
|
||||
height: 520rpx;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<view class="page dark:text-[var(--wot-color-text)]">
|
||||
<view class="page-container dark:text-[var(--wot-color-text)]">
|
||||
<view class="page-content">
|
||||
<view class="header">
|
||||
<image class="logo" src="/static/logo.png" mode="aspectFill" />
|
||||
@@ -45,8 +45,8 @@
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
padding: 28rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
@@ -77,8 +77,8 @@
|
||||
}
|
||||
|
||||
.group-tip {
|
||||
padding: 20rpx;
|
||||
margin-top: 20rpx;
|
||||
padding: 20rpx;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.6;
|
||||
color: #374151;
|
||||
@@ -87,8 +87,8 @@
|
||||
}
|
||||
|
||||
.qr-card {
|
||||
padding: 28rpx;
|
||||
margin-top: 24rpx;
|
||||
padding: 28rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
@@ -101,8 +101,8 @@
|
||||
|
||||
.qr-tip {
|
||||
margin-top: 20rpx;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: #6b7280;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<view class="page page--auto dark:text-[var(--wot-color-text)]">
|
||||
<view class="page page--full dark:text-[var(--wot-color-text)]">
|
||||
<view class="profile-content">
|
||||
<view class="pt-20rpx">
|
||||
<wd-card v-if="userProfile">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<view class="page dark:text-[var(--wot-color-text)]">
|
||||
<view class="page-container dark:text-[var(--wot-color-text)]">
|
||||
<wd-collapse v-model="activeNames" accordion>
|
||||
<wd-collapse-item
|
||||
v-for="(section, index) in agreementContent"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<view class="page dark:text-[var(--wot-color-text)]">
|
||||
<view class="page-container dark:text-[var(--wot-color-text)]">
|
||||
<wd-cell-group>
|
||||
<wd-cell title="主题设置" icon="setting1" is-link @click="navigateToTheme" />
|
||||
<wd-cell title="用户协议" icon="user" is-link @click="navigateToUserAgreement" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<view class="page dark:text-[var(--wot-color-text)]">
|
||||
<view class="page-container dark:text-[var(--wot-color-text)]">
|
||||
<wd-collapse v-model="activeNames" accordion>
|
||||
<wd-collapse-item
|
||||
v-for="(section, index) in privacyContent"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<view class="page dark:text-[var(--wot-color-text)]">
|
||||
<view class="page-container dark:text-[var(--wot-color-text)]">
|
||||
<!-- 暗黑模式设置 -->
|
||||
<wd-card custom-class="mx-30rpx my-20rpx">
|
||||
<template #header>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="work">
|
||||
<!-- 筛选 -->
|
||||
<wd-drop-menu close-on-click-modal class="mb-20rpx mr-20rpx ml-20rpx">
|
||||
<wd-drop-menu-item ref="dropMenu" title="筛选" icon="filter" icon-size="18px">
|
||||
|
||||
@@ -1,27 +1,5 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<!-- 统计卡片 -->
|
||||
<view class="stats-row">
|
||||
<view class="stat-card">
|
||||
<view class="stat-card__icon stat-card__icon--primary">
|
||||
<wd-icon name="organization" size="20" />
|
||||
</view>
|
||||
<view class="stat-card__content">
|
||||
<text class="stat-card__value">{{ totalDepts }}</text>
|
||||
<text class="stat-card__label">部门总数</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<view class="stat-card__icon stat-card__icon--success">
|
||||
<wd-icon name="people" size="20" />
|
||||
</view>
|
||||
<view class="stat-card__content">
|
||||
<text class="stat-card__value">{{ totalUsers }}</text>
|
||||
<text class="stat-card__label">员工总数</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="dept-page">
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar">
|
||||
<wd-search
|
||||
@@ -33,39 +11,28 @@
|
||||
</view>
|
||||
|
||||
<!-- 部门列表 -->
|
||||
<scroll-view
|
||||
class="dept-list"
|
||||
scroll-y
|
||||
refresher-enabled
|
||||
:refresher-triggered="isRefreshing"
|
||||
@refresherrefresh="onRefresh"
|
||||
>
|
||||
<scroll-view class="dept-list" scroll-y>
|
||||
<view v-if="filteredDeptList.length > 0" class="dept-tree">
|
||||
<template v-for="dept in filteredDeptList" :key="dept.id">
|
||||
<dept-item
|
||||
:dept="dept"
|
||||
:level="0"
|
||||
@click="handleDeptClick"
|
||||
@edit="handleEditDept"
|
||||
@delete="handleDeleteDept"
|
||||
/>
|
||||
</template>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-else class="empty-state">
|
||||
<view class="empty-state__icon-wrap">
|
||||
<wd-icon name="folder-open" size="64" color="var(--color-text-disabled)" />
|
||||
</view>
|
||||
<text class="empty-state__title">暂无部门</text>
|
||||
<text class="empty-state__desc">点击底部按钮创建第一个部门</text>
|
||||
<wd-icon name="folder-open" size="48" color="#d1d5db" />
|
||||
<text class="empty-state__text">暂无部门数据</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="action-bar">
|
||||
<wd-button type="primary" block @click="handleAddDept">
|
||||
<wd-icon name="add" size="18" color="#fff" />
|
||||
<wd-icon name="add" size="16" color="#fff" />
|
||||
<text>新增部门</text>
|
||||
</wd-button>
|
||||
</view>
|
||||
@@ -77,18 +44,15 @@
|
||||
* 部门管理页面
|
||||
*
|
||||
* 功能说明:
|
||||
* - 部门树形结构展示
|
||||
* - 部门统计概览
|
||||
* - 搜索过滤
|
||||
* - 下拉刷新
|
||||
* - 滑动操作(编辑/删除)
|
||||
* - 新增部门
|
||||
* - 展示部门树形结构
|
||||
* - 支持搜索部门
|
||||
* - 新增/编辑/删除部门
|
||||
*
|
||||
* 技术要点:
|
||||
* - 使用 BEM 命名规范
|
||||
* - 支持暗黑模式
|
||||
* - CSS 变量
|
||||
* - 递归组件
|
||||
* - 使用 CSS 变量
|
||||
* - 递归组件实现树形结构
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
@@ -96,14 +60,14 @@
|
||||
// ============================================================================
|
||||
|
||||
interface DeptNode {
|
||||
id: string;;
|
||||
label: string;;
|
||||
userCount?: number;;
|
||||
children?: DeptNode[];;
|
||||
id: string
|
||||
label: string
|
||||
userCount?: number
|
||||
children?: DeptNode[]
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 子组件 - 部门项(支持递归 + 滑动操作)
|
||||
// 子组件 - 部门项(支持递归)
|
||||
// ============================================================================
|
||||
|
||||
const DeptItem = defineComponent({
|
||||
@@ -112,91 +76,55 @@ const DeptItem = defineComponent({
|
||||
dept: { type: Object as PropType<DeptNode>, required: true },
|
||||
level: { type: Number, default: 0 },
|
||||
},
|
||||
emits: ["click", "edit", "delete"],
|
||||
emits: ["click"],
|
||||
setup(props, { emit }) {
|
||||
const isExpanded = ref(false);;
|
||||
const isSwipeOpen = ref(false);;
|
||||
const hasChildren = computed(() => props.dept.children && props.dept.children.length > 0);;
|
||||
const isExpanded = ref(false)
|
||||
const hasChildren = computed(() => props.dept.children && props.dept.children.length > 0)
|
||||
|
||||
function toggleExpand() {
|
||||
if (hasChildren.value) {
|
||||
isExpanded.value = !isExpanded.value;;
|
||||
isExpanded.value = !isExpanded.value
|
||||
}
|
||||
}
|
||||
|
||||
function handleClick() {
|
||||
emit("click", props.dept);;
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
emit("edit", props.dept);;
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
emit("delete", props.dept);;
|
||||
}
|
||||
|
||||
function closeSwipe() {
|
||||
isSwipeOpen.value = false;;
|
||||
emit("click", props.dept)
|
||||
}
|
||||
|
||||
return {
|
||||
isExpanded,
|
||||
isSwipeOpen,
|
||||
hasChildren,
|
||||
toggleExpand,
|
||||
handleClick,
|
||||
handleEdit,
|
||||
handleDelete,
|
||||
closeSwipe,
|
||||
};;
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<view class="dept-node">
|
||||
<view class="dept-node__swipe-wrap">
|
||||
<view class="dept-node__actions">
|
||||
<view class="dept-node__action dept-node__action--edit" @click="handleEdit">
|
||||
<wd-icon name="edit" size="16" color="#fff" />
|
||||
<text>编辑</text>
|
||||
</view>
|
||||
<view class="dept-node__action dept-node__action--delete" @click="handleDelete">
|
||||
<wd-icon name="delete" size="16" color="#fff" />
|
||||
<text>删除</text>
|
||||
</view>
|
||||
<view
|
||||
class="dept-node__item"
|
||||
:class="{ 'dept-node__item--has-children': hasChildren }"
|
||||
:style="{ paddingLeft: level * 32 + 16 + 'rpx' }"
|
||||
>
|
||||
<view class="dept-node__expand" @click="toggleExpand">
|
||||
<wd-icon
|
||||
v-if="hasChildren"
|
||||
name="arrow-right"
|
||||
size="14"
|
||||
class="dept-node__arrow"
|
||||
:class="{ 'dept-node__arrow--expanded': isExpanded }"
|
||||
/>
|
||||
</view>
|
||||
<view
|
||||
class="dept-node__item"
|
||||
:class="{ 'dept-node__item--has-children': hasChildren }"
|
||||
:style="{ paddingLeft: level * 24 + 16 + 'rpx' }"
|
||||
@click="handleClick"
|
||||
>
|
||||
<view class="dept-node__expand" @click.stop="toggleExpand">
|
||||
<wd-icon
|
||||
v-if="hasChildren"
|
||||
name="arrow-right"
|
||||
size="14"
|
||||
class="dept-node__arrow"
|
||||
:class="{ 'dept-node__arrow--expanded': isExpanded }"
|
||||
/>
|
||||
</view>
|
||||
<view class="dept-node__content">
|
||||
<view
|
||||
class="dept-node__icon"
|
||||
:class="{ 'dept-node__icon--has-children': hasChildren }"
|
||||
>
|
||||
<wd-icon
|
||||
:name="hasChildren ? 'folder' : 'folder-open'"
|
||||
size="18"
|
||||
/>
|
||||
</view>
|
||||
<view class="dept-node__info">
|
||||
<text class="dept-node__name">{{ dept.label }}</text>
|
||||
<text class="dept-node__count">{{ dept.userCount || 0 }} 人</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="dept-node__chevron">
|
||||
<wd-icon name="chevron-right" size="14" />
|
||||
</view>
|
||||
<view class="dept-node__content" @click="handleClick">
|
||||
<wd-icon
|
||||
name="folder"
|
||||
size="18"
|
||||
:color="hasChildren ? 'var(--color-primary)' : 'var(--color-text-secondary)'"
|
||||
/>
|
||||
<text class="dept-node__name">{{ dept.label }}</text>
|
||||
</view>
|
||||
<view class="dept-node__meta" @click="handleClick">
|
||||
<text class="dept-node__count">{{ dept.userCount || 0 }} 人</text>
|
||||
<wd-icon name="arrow-right" size="14" color="var(--color-text-secondary)" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -208,22 +136,19 @@ const DeptItem = defineComponent({
|
||||
:dept="child"
|
||||
:level="level + 1"
|
||||
@click="$emit('click', $event)"
|
||||
@edit="$emit('edit', $event)"
|
||||
@delete="$emit('delete', $event)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
`,
|
||||
});;
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// 响应式数据
|
||||
// ============================================================================
|
||||
|
||||
const searchText = ref("");;
|
||||
const isRefreshing = ref(false);;
|
||||
const searchText = ref("")
|
||||
|
||||
// 部门树形数据
|
||||
// 部门树形数据(示例数据)
|
||||
const deptList = ref<DeptNode[]>([
|
||||
{
|
||||
id: "1",
|
||||
@@ -261,100 +186,47 @@ const deptList = ref<DeptNode[]>([
|
||||
},
|
||||
],
|
||||
},
|
||||
]);;
|
||||
])
|
||||
|
||||
// 统计数据
|
||||
const totalDepts = computed(() => {
|
||||
function countDepts(depts: DeptNode[]): number {
|
||||
return depts.reduce(
|
||||
|
||||
|
||||
;
|
||||
(sum, dept) => sum + 1 + (dept.children ? countDepts(dept.children) : 0),
|
||||
0;
|
||||
; );
|
||||
}
|
||||
return countDepts(deptList.value);
|
||||
});
|
||||
|
||||
|
||||
|
||||
;
|
||||
const totalUsers = computed(() => {
|
||||
function sumUsers(depts: DeptNo;de[]): number {
|
||||
; return depts.reduce(
|
||||
(sum, dept) => sum + (dept.userCount || 0) + (dept.children ? sumUsers(dept.children) : 0),
|
||||
0
|
||||
);
|
||||
};
|
||||
return sumUsers(deptList.value);
|
||||
});
|
||||
|
||||
// 过滤后的部门列表;
|
||||
const filteredDeptList = computed(() => {;
|
||||
if (!searchText.value) return deptList.value;
|
||||
// 过滤后的部门列表
|
||||
const filteredDeptList = computed(() => {
|
||||
if (!searchText.value) return deptList.value
|
||||
|
||||
function filterDepts(depts: DeptNode[]): DeptNode[] {
|
||||
return depts.reduce<DeptNode[]>((acc, dept) => {
|
||||
const matched = dept.label.toLowerCase().includes(searchText.value.toLowerCase());
|
||||
cons;t filteredChildren = dept.children ? filterDepts(dept.children) : [];
|
||||
const matched = dept.label.toLowerCase().includes(searchText.value.toLowerCase())
|
||||
const filteredChildren = dept.children ? filterDepts(dept.children) : []
|
||||
|
||||
if (matche;d || filteredChildren.length > 0) {
|
||||
ac;c.push({
|
||||
if (matched || filteredChildren.length > 0) {
|
||||
acc.push({
|
||||
...dept,
|
||||
children: filteredChildren.length > 0 ? filteredChildren : dept.children,
|
||||
});;
|
||||
; }
|
||||
return acc;
|
||||
}, []);
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
}
|
||||
|
||||
return filterDepts(deptList.value);
|
||||
});
|
||||
return filterDepts(deptList.value)
|
||||
})
|
||||
|
||||
// ========================;====================================================
|
||||
// 事件处理;
|
||||
// =========================;===================================================
|
||||
;
|
||||
/** 下拉刷新 */
|
||||
async function onRefresh() {
|
||||
isRefreshing.value = true;
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
isRefreshing.value = false;;
|
||||
uni.showToast({ title: "刷新成功", icon: "success" });
|
||||
}
|
||||
// ============================================================================
|
||||
// 事件处理
|
||||
// ============================================================================
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {;
|
||||
uni.showToast({ title: `搜索: ${searchText.value}`, icon: "none" });
|
||||
function handleSearch() {
|
||||
uni.showToast({ title: `搜索: ${searchText.value}`, icon: "none" })
|
||||
}
|
||||
|
||||
/** 部门点击 */
|
||||
function handleDeptClick(dept: DeptNode) {;
|
||||
uni.showToast({ title: `查看: ${dept.label}`, icon: "none" });
|
||||
}
|
||||
|
||||
/** 编辑部门 */
|
||||
function handleEditDept(dept: DeptNode) {
|
||||
uni.showToast({ title: `编辑: ${dept.label}`, icon: "none" });
|
||||
}
|
||||
|
||||
/** 删除部门 */
|
||||
function handleDeleteDept(dept: DeptNode) {;
|
||||
uni.showModal({
|
||||
title: "确认删除",
|
||||
;content: `确定要删除部门"${dept.label}"吗?`,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showToast({ title: "删除成功", icon: "success" });
|
||||
}
|
||||
},;
|
||||
});
|
||||
function handleDeptClick(dept: DeptNode) {
|
||||
uni.showToast({ title: `点击: ${dept.label}`, icon: "none" })
|
||||
}
|
||||
|
||||
/** 新增部门 */
|
||||
function handleAddDept() {
|
||||
uni.showToast({ title: "新增部门", icon: "none" });
|
||||
uni.showToast({ title: "新增部门", icon: "none" })
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -370,70 +242,13 @@ function handleAddDept() {
|
||||
// 页面容器
|
||||
// ============================================================================
|
||||
|
||||
.page {
|
||||
.dept-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
background-color: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 统计卡片
|
||||
// ============================================================================
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
padding: 24rpx;
|
||||
background-color: var(--color-bg);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 20rpx;
|
||||
align-items: center;
|
||||
padding: 28rpx;
|
||||
background-color: var(--color-bg-secondary);
|
||||
border-radius: 16rpx;
|
||||
|
||||
&__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 16rpx;
|
||||
|
||||
&--primary {
|
||||
color: var(--color-primary);
|
||||
background-color: rgba(var(--color-primary-rgb), 0.1);
|
||||
}
|
||||
|
||||
&--success {
|
||||
color: #22c55e;
|
||||
background-color: rgba(34, 197, 94, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 搜索栏
|
||||
// ============================================================================
|
||||
@@ -456,47 +271,14 @@ function handleAddDept() {
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 部门节点
|
||||
// 部门节点(递归组件样式)
|
||||
// ============================================================================
|
||||
|
||||
.dept-node {
|
||||
&__swipe-wrap {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__action {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 120rpx;
|
||||
font-size: 22rpx;
|
||||
color: #fff;
|
||||
|
||||
&--edit {
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
|
||||
&--delete {
|
||||
background-color: #ef4444;
|
||||
}
|
||||
}
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 112rpx;
|
||||
padding-right: 24rpx;
|
||||
background-color: var(--color-bg);
|
||||
height: 96rpx;
|
||||
border-bottom: 2rpx solid var(--color-border-light);
|
||||
|
||||
&:active {
|
||||
@@ -508,8 +290,8 @@ function handleAddDept() {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
}
|
||||
|
||||
&__arrow {
|
||||
@@ -524,47 +306,27 @@ function handleAddDept() {
|
||||
&__content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 20rpx;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
color: var(--color-text-secondary);
|
||||
background-color: var(--color-bg-secondary);
|
||||
border-radius: 12rpx;
|
||||
|
||||
&--has-children {
|
||||
color: var(--color-primary);
|
||||
background-color: rgba(var(--color-primary-rgb), 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
font-size: 28rpx;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding-right: 24rpx;
|
||||
}
|
||||
|
||||
&__count {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
&__chevron {
|
||||
color: var(--color-text-disabled);
|
||||
}
|
||||
|
||||
&__children {
|
||||
background-color: var(--color-bg-secondary);
|
||||
}
|
||||
@@ -579,21 +341,11 @@ function handleAddDept() {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 0;
|
||||
padding: 100rpx 0;
|
||||
|
||||
&__icon-wrap {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
&__desc {
|
||||
margin-top: 12rpx;
|
||||
font-size: 26rpx;
|
||||
&__text {
|
||||
margin-top: 24rpx;
|
||||
font-size: 28rpx;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import { useRouter } from "uni-mini-router";
|
||||
import { useUserStore } from "@/store";
|
||||
import { menuConfig } from "@/config/menu";
|
||||
@@ -61,7 +62,7 @@ function handleNavClick(item: any) {
|
||||
{
|
||||
"name": "work",
|
||||
"style": {
|
||||
"navigationBarTitleText": "工作å<EFBFBD>?
|
||||
"navigationBarTitleText": "工作台"
|
||||
},
|
||||
"layout": "tabbar"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="log">
|
||||
<!-- 筛选 -->
|
||||
<wd-drop-menu close-on-click-modal class="mb-24rpx">
|
||||
<wd-drop-menu-item ref="filterDropMenu" title="筛选" icon="filter" icon-size="18px">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view>
|
||||
<!-- 添加搜索栏 -->
|
||||
<wd-drop-menu close-on-click-modal class="mb-20rpx mr-20rpx ml-20rpx">
|
||||
<wd-drop-menu-item ref="dropMenu" title="筛选" icon="filter" icon-size="18px">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="role">
|
||||
<!-- 筛选 -->
|
||||
<wd-drop-menu>
|
||||
<wd-drop-menu-item ref="filterDropMenu" icon="filter" icon-size="18px" title="筛选">
|
||||
|
||||
@@ -1,83 +1,118 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<!-- 排序筛选 -->
|
||||
<view class="filter-container">
|
||||
<wd-drop-menu>
|
||||
<wd-drop-menu-item
|
||||
v-model="sortValue"
|
||||
:options="sortOptions"
|
||||
title="排序"
|
||||
icon="unfold-more"
|
||||
@change="handleSortChange"
|
||||
<view class="page page--full page--padding">
|
||||
<!-- 搜索筛选 -->
|
||||
<view class="filter">
|
||||
<view class="filter__search">
|
||||
<wd-search
|
||||
v-model="queryParams.keywords"
|
||||
placeholder="搜索用户名/手机号"
|
||||
hide-cancel
|
||||
@search="handleQuery"
|
||||
/>
|
||||
<wd-drop-menu-item ref="filterDropMenu" title="筛选" icon="filter">
|
||||
<view>
|
||||
<wd-input
|
||||
v-model="queryParams.keywords"
|
||||
label="关键字"
|
||||
placeholder="用户名/昵称/手机号"
|
||||
/>
|
||||
|
||||
<cu-date-query v-model="queryParams.createTime" label="创建时间" />
|
||||
|
||||
<view class="flex-between py-2">
|
||||
<wd-button type="info" @click="handleResetQuery">重置</wd-button>
|
||||
<wd-button @click="handleQuery">查询</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-drop-menu-item>
|
||||
</wd-drop-menu>
|
||||
</view>
|
||||
<view class="filter__bar">
|
||||
<view class="filter__item" @click="sortSheetVisible = true">
|
||||
<text class="filter__text">排序</text>
|
||||
<wd-icon name="arrow-down" size="14" color="#64748b" />
|
||||
</view>
|
||||
<view class="filter__divider" />
|
||||
<view class="filter__item" @click="filterVisible = true">
|
||||
<text class="filter__text">筛选</text>
|
||||
<wd-icon name="filter" size="14" color="#64748b" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<wd-action-sheet
|
||||
v-model="sortSheetVisible"
|
||||
:actions="sortActions"
|
||||
cancel-text="取消"
|
||||
@select="handleSortSelect"
|
||||
/>
|
||||
|
||||
<wd-popup
|
||||
v-model="filterVisible"
|
||||
position="bottom"
|
||||
custom-style="border-top-left-radius: 24rpx; border-top-right-radius: 24rpx;"
|
||||
>
|
||||
<view class="filter-panel">
|
||||
<wd-input v-model="queryParams.keywords" label="关键字" placeholder="用户名/昵称/手机号" />
|
||||
<cu-date-query v-model="queryParams.createTime" label="创建时间" />
|
||||
<view class="filter-panel__footer">
|
||||
<wd-button type="info" @click="handleResetQuery">重置</wd-button>
|
||||
<wd-button type="primary" @click="handleQuery">查询</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
||||
<!-- 数据列表 -->
|
||||
<view class="data-container">
|
||||
<wd-card v-for="item in pageData" :key="item.id">
|
||||
<template #title>
|
||||
<view class="flex-between">
|
||||
<view class="flex-center">
|
||||
<wd-img :width="50" :height="50" round :src="item.avatar" />
|
||||
<view class="ml-2">
|
||||
<view class="font-bold">
|
||||
{{ item.nickname }}
|
||||
<wd-icon v-if="item.gender == 1" name="gender-male" class="color-#4D80F0" />
|
||||
<wd-icon
|
||||
v-else-if="item.gender == 2"
|
||||
name="gender-female"
|
||||
class="color-#FA4350"
|
||||
/>
|
||||
<view class="list">
|
||||
<view v-for="item in pageData" :key="item.id" class="list__card">
|
||||
<wd-card>
|
||||
<template #title>
|
||||
<view class="flex-between">
|
||||
<view class="flex-center">
|
||||
<wd-img :width="50" :height="50" round :src="item.avatar" />
|
||||
<view class="ml-2">
|
||||
<view class="font-bold">
|
||||
{{ item.nickname }}
|
||||
<wd-icon v-if="item.gender == 1" name="gender-male" class="color-#4D80F0" />
|
||||
<wd-icon
|
||||
v-else-if="item.gender == 2"
|
||||
name="gender-female"
|
||||
class="color-#FA4350"
|
||||
/>
|
||||
</view>
|
||||
<view class="mt-1"><wd-text :text="item.deptName" size="12px" /></view>
|
||||
</view>
|
||||
<view class="mt-1"><wd-text :text="item.deptName" size="12px" /></view>
|
||||
</view>
|
||||
<view>
|
||||
<wd-tag v-if="item.status === 1" type="success" plain>正常</wd-tag>
|
||||
<wd-tag v-else-if="item.status === 0" plain>禁用</wd-tag>
|
||||
</view>
|
||||
</view>
|
||||
<view>
|
||||
<wd-tag v-if="item.status === 1" type="success" plain>正常</wd-tag>
|
||||
<wd-tag v-else-if="item.status === 0" plain>禁用</wd-tag>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<wd-cell-group>
|
||||
<wd-cell title="用户名" :value="item.username" icon="user" />
|
||||
<wd-cell title="角色" :value="item.roleNames" icon="usergroup" />
|
||||
<wd-cell title="手机号码" :value="item.mobile" icon="mobile" />
|
||||
<wd-cell title="邮箱" :value="item.email" icon="mail" />
|
||||
</wd-cell-group>
|
||||
<wd-cell-group>
|
||||
<wd-cell title="用户名" :value="item.username" icon="user" />
|
||||
<wd-cell title="角色" :value="item.roleNames" icon="usergroup" />
|
||||
<wd-cell title="手机号码" :value="item.mobile" icon="mobile" />
|
||||
<wd-cell title="邮箱" :value="item.email" icon="mail" />
|
||||
</wd-cell-group>
|
||||
|
||||
<template #footer>
|
||||
<view class="flex-between">
|
||||
<view class="text-left">
|
||||
<wd-text text="创建时间:" size="small" class="font-bold" />
|
||||
<wd-text :text="item.createTime" size="small" />
|
||||
</view>
|
||||
<view class="text-right">
|
||||
<Auth permission="sys:user:update">
|
||||
<wd-button 编辑 wd-button> bsp; th>uth permission="sys:user:delete">
|
||||
<wd-button type="error" size="small" plain @click="handleDelete(item.id)">
|
||||
<template #footer>
|
||||
<view class="flex-between">
|
||||
<view class="text-left">
|
||||
<wd-text text="创建时间:" size="small" class="font-bold" />
|
||||
<wd-text :text="item.createTime" size="small" />
|
||||
</view>
|
||||
<view class="text-right">
|
||||
<wd-button
|
||||
v-if="hasPermission('sys:user:update')"
|
||||
type="primary"
|
||||
size="small"
|
||||
plain
|
||||
@click="handleOpenDialog(item.id)"
|
||||
>
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasPermission('sys:user:delete')"
|
||||
type="error"
|
||||
size="small"
|
||||
plain
|
||||
@click="handleDelete(item.id)"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
</Auth>
|
||||
</view>
|
||||
</view>
|
||||
</view>e>e v-if="total > 0" :state="load-tip v-else-if="total == 0" image="search" tip="当前搜索无结果" />
|
||||
</template>
|
||||
</wd-card>
|
||||
</view>
|
||||
|
||||
<wd-loadmore v-if="total > 0" :state="loadMoreState" @reload="loadmore" />
|
||||
<wd-status-tip v-else-if="total == 0" image="search" tip="当前搜索无结果" />
|
||||
</view>
|
||||
|
||||
<!-- 弹窗表单 -->
|
||||
@@ -111,14 +146,13 @@
|
||||
</wd-popup>
|
||||
|
||||
<!-- 悬浮操作按钮 -->
|
||||
<Auth permission="sys:user:create">
|
||||
<wd-fab
|
||||
position="left-bottom"
|
||||
:expandable="false"
|
||||
custom-style="z-index: 9"
|
||||
@click="handleOpenDialog"
|
||||
/>
|
||||
</Auth>
|
||||
<wd-fab
|
||||
v-if="hasPermission('sys:user:create')"
|
||||
position="left-bottom"
|
||||
:expandable="false"
|
||||
custom-style="z-index: 9"
|
||||
@click="handleOpenDialog"
|
||||
/>
|
||||
|
||||
<wd-message-box />
|
||||
</view>
|
||||
@@ -134,12 +168,15 @@ import { useMessage } from "wot-design-uni";
|
||||
import UserAPI, { type UserPageQuery, UserPageVO, UserForm } from "@/api/user";
|
||||
import RoleAPI from "@/api/role";
|
||||
import DeptAPI from "@/api/dept";
|
||||
import { hasPermission } from "@/utils/permission";
|
||||
|
||||
const message = useMessage();
|
||||
const loadMoreState = ref<LoadMoreState>("loading");
|
||||
const filterDropMenu = ref();
|
||||
const userFormRef = ref();
|
||||
|
||||
const sortSheetVisible = ref(false);
|
||||
const filterVisible = ref(false);
|
||||
|
||||
const sortValue = ref(0);
|
||||
const sortOptions = ref<Record<string, any>[]>([
|
||||
{ label: "默认排序", value: 0 },
|
||||
@@ -147,9 +184,14 @@ const sortOptions = ref<Record<string, any>[]>([
|
||||
{ label: "最近更新", value: 2 },
|
||||
]);
|
||||
|
||||
const sortActions = computed(() =>
|
||||
sortOptions.value.map((item) => ({ name: item.label, value: item.value }))
|
||||
);
|
||||
|
||||
let queryParams: UserPageQuery = {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
keywords: "",
|
||||
};
|
||||
|
||||
const total = ref(0);
|
||||
@@ -232,11 +274,16 @@ const handleSortChange = (event: { value: string | number }) => {
|
||||
handleQuery();
|
||||
};
|
||||
|
||||
const handleSortSelect = (action: { name: string; value: string | number }) => {
|
||||
sortValue.value = Number(action.value);
|
||||
handleSortChange({ value: action.value });
|
||||
};
|
||||
|
||||
/**
|
||||
* 查询
|
||||
*/
|
||||
const handleQuery = () => {
|
||||
filterDropMenu.value?.close();
|
||||
filterVisible.value = false;
|
||||
queryParams.pageNum = 1;
|
||||
loadmore();
|
||||
};
|
||||
@@ -248,6 +295,7 @@ const handleResetQuery = () => {
|
||||
queryParams = {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
keywords: "",
|
||||
};
|
||||
handleQuery();
|
||||
};
|
||||
@@ -371,13 +419,17 @@ export default {
|
||||
{
|
||||
"name": "user",
|
||||
"style": {
|
||||
"navigationBarTitleText": "用户管理"
|
||||
"navigationBarTitleText": "用户管理",
|
||||
"backgroundColor": "#f1f5f9"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user-container {
|
||||
min-height: 100vh;
|
||||
background: #f1f5f9;
|
||||
|
||||
:deep(.wd-cell__wrapper) {
|
||||
padding: 4rpx 0;
|
||||
}
|
||||
@@ -391,18 +443,5 @@ export default {
|
||||
width: 80rpx !important;
|
||||
height: 80rpx !important;
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
padding: 10rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.data-container {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.popup-footer {
|
||||
margin: 30rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -45,14 +45,10 @@ body,
|
||||
// ==========================================================================
|
||||
|
||||
.filter {
|
||||
padding: 16rpx 0;
|
||||
|
||||
&__search {
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(15, 23, 42, 0.04);
|
||||
|
||||
:deep(.wd-search) {
|
||||
background: transparent;
|
||||
@@ -65,9 +61,7 @@ body,
|
||||
height: 88rpx;
|
||||
margin-top: 12rpx;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
&__item {
|
||||
@@ -75,7 +69,6 @@ body,
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__text {
|
||||
@@ -121,6 +114,44 @@ body,
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__meta {
|
||||
padding: 8rpx 24rpx 4rpx;
|
||||
}
|
||||
|
||||
&__meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 72rpx;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
&__meta-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&__meta-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
&__meta-label {
|
||||
font-size: 26rpx;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
&__meta-value {
|
||||
max-width: 58%;
|
||||
overflow: hidden;
|
||||
font-size: 26rpx;
|
||||
color: #0f172a;
|
||||
text-align: right;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
|
||||
Reference in New Issue
Block a user