refactor: 界面和样式优化

This commit is contained in:
Ray.Hao
2026-03-10 20:37:49 +08:00
parent 7ce897744a
commit 53c2985220
30 changed files with 670 additions and 707 deletions

2
components.d.ts vendored
View File

@@ -19,6 +19,7 @@ declare module 'vue' {
QiunDataCharts: typeof import('./src/components/qiun-data-charts/qiun-data-charts.vue')['default'] QiunDataCharts: typeof import('./src/components/qiun-data-charts/qiun-data-charts.vue')['default']
QiunError: typeof import('./src/components/qiun-error/qiun-error.vue')['default'] QiunError: typeof import('./src/components/qiun-error/qiun-error.vue')['default']
QiunLoading: typeof import('./src/components/qiun-loading/qiun-loading.vue')['default'] QiunLoading: typeof import('./src/components/qiun-loading/qiun-loading.vue')['default']
WdActionSheet: typeof import('wot-design-uni/components/wd-action-sheet/wd-action-sheet.vue')['default']
WdBadge: typeof import('wot-design-uni/components/wd-badge/wd-badge.vue')['default'] WdBadge: typeof import('wot-design-uni/components/wd-badge/wd-badge.vue')['default']
WdButton: typeof import('wot-design-uni/components/wd-button/wd-button.vue')['default'] WdButton: typeof import('wot-design-uni/components/wd-button/wd-button.vue')['default']
WdCalendar: typeof import('wot-design-uni/components/wd-calendar/wd-calendar.vue')['default'] WdCalendar: typeof import('wot-design-uni/components/wd-calendar/wd-calendar.vue')['default']
@@ -44,7 +45,6 @@ declare module 'vue' {
WdLoading: typeof import('wot-design-uni/components/wd-loading/wd-loading.vue')['default'] WdLoading: typeof import('wot-design-uni/components/wd-loading/wd-loading.vue')['default']
WdLoadmore: typeof import('wot-design-uni/components/wd-loadmore/wd-loadmore.vue')['default'] WdLoadmore: typeof import('wot-design-uni/components/wd-loadmore/wd-loadmore.vue')['default']
WdMessageBox: typeof import('wot-design-uni/components/wd-message-box/wd-message-box.vue')['default'] WdMessageBox: typeof import('wot-design-uni/components/wd-message-box/wd-message-box.vue')['default']
WdNoticeBar: typeof import('wot-design-uni/components/wd-notice-bar/wd-notice-bar.vue')['default']
WdNotify: typeof import('wot-design-uni/components/wd-notify/wd-notify.vue')['default'] WdNotify: typeof import('wot-design-uni/components/wd-notify/wd-notify.vue')['default']
WdPicker: typeof import('wot-design-uni/components/wd-picker/wd-picker.vue')['default'] WdPicker: typeof import('wot-design-uni/components/wd-picker/wd-picker.vue')['default']
WdPopup: typeof import('wot-design-uni/components/wd-popup/wd-popup.vue')['default'] WdPopup: typeof import('wot-design-uni/components/wd-popup/wd-popup.vue')['default']

View File

@@ -1,91 +0,0 @@
<template>
<slot v-if="authorized" />
<slot v-else name="fallback" />
</template>
<script lang="ts" setup>
/**
* 权限控制组件
*
* @description 跨平台权限控制组件,支持权限和角色检查
* @usage 适用于所有平台(H5、小程序、App)
*
* @example
* // 权限检查
* <Auth permission="sys:user:create">
* <button>新增用户</button>
* </Auth>
*
* // 多个权限(满足其一即可)
* <Auth :permission="['sys:user:create', 'sys:user:update']">
* <button>操作</button>
* </Auth>
*
* // 角色检查
* <Auth role="ADMIN">
* <button>管理员操作</button>
* </Auth>
*
* // 同时检查权限和角色
* <Auth permission="sys:user:delete" role="ADMIN">
* <button>删除用户</button>
* </Auth>
*
* // 无权限时显示备用内容
* <Auth permission="sys:user:create">
* <template #default>
* <button>新增用户</button>
* </template>
* <template #fallback>
* <text>无权限</text>
* </template>
* </Auth>
*/
import { computed } from "vue";
import { hasPermission, hasRole } from "@/utils/permission";
interface Props {
/** 权限标识,支持字符串或字符串数组 */
permission?: string | string[];
/** 角色标识,支持字符串或字符串数组 */
role?: string | string[];
/** 是否需要同时满足权限和角色(默认:false,满足其一即可) */
requireAll?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
permission: undefined,
role: undefined,
requireAll: false,
});
/**
* 计算是否有权限
*/
const authorized = computed(() => {
const { permission, role, requireAll } = props;
// 无任何限制条件,默认显示
if (!permission && !role) {
return true;
}
// 检查权限
const hasPerm = permission ? hasPermission(permission) : true;
// 检查角色
const hasRoleFlag = role ? hasRole(role) : true;
// 根据条件返回结果
if (requireAll) {
// 需要同时满足
return hasPerm && hasRoleFlag;
} else {
// 满足其一即可
if (permission && role) {
return hasPerm || hasRoleFlag;
}
return hasPerm && hasRoleFlag;
}
});
</script>

View File

@@ -132,7 +132,8 @@
"type": "page", "type": "page",
"name": "user", "name": "user",
"style": { "style": {
"navigationBarTitleText": "用户管理" "navigationBarTitleText": "用户管理",
"backgroundColor": "#f1f5f9"
} }
}, },
{ {

View File

@@ -1,6 +1,6 @@
<template> <template>
<view class="home-page"> <view class="home-page">
<!-- 轮播图 --> <!-- 頧格偘<EFBFBD>?-->
<view class="hero"> <view class="hero">
<wd-swiper <wd-swiper
v-model:current="current" v-model:current="current"
@@ -30,13 +30,19 @@
</view> </view>
<!-- <EFBFBD>𡁶䰻<EFBFBD>砍<EFBFBD> --> <!-- <EFBFBD>𡁶䰻<EFBFBD>砍<EFBFBD> -->
<view class="section"> <view v-if="noticeList.length" class="section">
<view class="notice-bar" @click="handleNoticeClick"> <view class="notice-bar" @click="handleNoticeClick">
<view class="notice-bar__icon"> <view class="notice-bar__icon">
<wd-icon name="check-outline" size="32rpx" color="#34D19D" /> <wd-icon name="check-outline" size="32rpx" color="#34D19D" />
</view> </view>
<view class="notice-bar__content"> <view class="notice-bar__content">
<text class="notice-bar__text">{{ noticeText || "暂无通知公告" }}</text> <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>
</view> </view>
</view> </view>
</view> </view>
@@ -49,7 +55,7 @@
<view class="stat-card__icon stat-card__icon--user"></view> <view class="stat-card__icon stat-card__icon--user"></view>
</view> </view>
<view class="stat-card__head"> <view class="stat-card__head">
<text class="stat-card__label">访客数</text> <text class="stat-card__label">霈踹恥<EFBFBD>?/text>
<view class="stat-card__dot stat-card__dot--green"></view> <view class="stat-card__dot stat-card__dot--green"></view>
</view> </view>
<text class="stat-card__num stat-card__num--green"> <text class="stat-card__num stat-card__num--green">
@@ -61,7 +67,7 @@
<view class="stat-card__icon stat-card__icon--eye"></view> <view class="stat-card__icon stat-card__icon--eye"></view>
</view> </view>
<view class="stat-card__head"> <view class="stat-card__head">
<text class="stat-card__label">浏览量</text> <text class="stat-card__label">瘚讛<EFBFBD><EFBFBD>?/text>
<view class="stat-card__dot stat-card__dot--blue"></view> <view class="stat-card__dot stat-card__dot--blue"></view>
</view> </view>
<text class="stat-card__num stat-card__num--blue"> <text class="stat-card__num stat-card__num--blue">
@@ -93,8 +99,8 @@
inline inline
@change="handleDataRangeChange" @change="handleDataRangeChange"
> >
<wd-radio :value="7">近7天</wd-radio> <wd-radio :value="7">餈?憭?/wd-radio>
<wd-radio :value="15">近15天</wd-radio> <wd-radio :value="15">餈?5憭?/wd-radio>
</wd-radio-group> </wd-radio-group>
</view> </view>
</template> </template>
@@ -111,19 +117,15 @@
/** /**
* 擐㚚△ * 擐㚚△
* *
* 功能说明: * <EFBFBD>蠘<EFBFBD>霂湔<EFBFBD>嚗? * - <20>芸<EFBFBD>銋匧紡<E58CA7>芣<EFBFBD>嚗<EFBFBD><E59A97>蝝X<E89D9D>嚗? * - 頧格偘<E6A0BC>曉<EFBFBD>蝷? * - 敹急㭘撖潸⏛<E6BDB8>亙藁
* - 自定义导航栏(搜索框)
* - 轮播图展示
* - 快捷导航入口
* - <20>𡁶䰻<F0A181B6>砍<EFBFBD> * - <20>𡁶䰻<F0A181B6>砍<EFBFBD>
* - <20>唳旿蝏蠘恣 * - <20>唳旿蝏蠘恣
* - 霈輸䔮頞见飵<E8A781>曇” * - 霈輸䔮頞见飵<E8A781>曇”
* *
* <20><><EFBFBD>航<EFBFBD><E888AA>對<EFBFBD> * <20><><EFBFBD>航<EFBFBD><E888AA>對<EFBFBD>
* - 使用 useNavbar 处理自定义导航栏高度和胶囊避让; * - 雿輻鍂 useNavbar 憭<EFBFBD><EFBFBD><EFBFBD>芸<EFBFBD>銋匧紡<EFBFBD>芣<EFBFBD>擃睃漲<EFBFBD>諹<EFBFBD><EFBFBD>𢠃<EFBFBD>霈?
* - 雿輻鍂 BEM <20>賢<EFBFBD>閫<EFBFBD><E996AB>蝏<EFBFBD><E89D8F> CSS * - 雿輻鍂 BEM <20>賢<EFBFBD>閫<EFBFBD><E996AB>蝏<EFBFBD><E89D8F> CSS
* - 支持暗黑模式(通过 CSS 变量) * - <EFBFBD>舀<EFBFBD><EFBFBD>烾<EFBFBD>璅∪<EFBFBD>嚗<EFBFBD><EFBFBD>朞<EFBFBD> CSS <20>㗛<EFBFBD>嚗? */
*/
import { computed, ref } from "vue"; import { computed, ref } from "vue";
import { onReady } from "@dcloudio/uni-app"; import { onReady } from "@dcloudio/uni-app";
@@ -154,11 +156,9 @@ interface NavItem {
const router = useRouter(); const router = useRouter();
const userStore = useUserStore(); const userStore = useUserStore();
// custom-navbar 组件内部已处理导航栏高度与胶囊避让 // custom-navbar 蝏<EFBFBD>辣<EFBFBD><EFBFBD><EFBFBD>撌脣<EFBFBD><EFBFBD><EFBFBD>紡<EFBFBD>芣<EFBFBD>擃睃漲銝舘<EFBFBD><EFBFBD>𢠃<EFBFBD>霈?
// ============================================================================
// 响应式数据
// ============================================================================ // ============================================================================
// <20>滚<EFBFBD>撘𤩺㺭<F0A4A9BA>?// ============================================================================
const current = ref(0); const current = ref(0);
const recentDaysRange = ref(7); const recentDaysRange = ref(7);
@@ -178,21 +178,19 @@ const appVersion = ref<string>("");
const noticeList = ref<NoticePageVO[]>([]); const noticeList = ref<NoticePageVO[]>([]);
const noticeText = computed(() => { const noticeText = computed(() => {
if (!noticeList.value.length) {
return "暂无通知";
}
const titles = noticeList.value const titles = noticeList.value
.map((n: NoticePageVO) => n.title) .map((n: NoticePageVO) => n.title)
.filter(Boolean) .filter(Boolean)
.slice(0, 2) as string[]; .slice(0, 5) as string[];
return titles.length ? titles.join(" ") : "暂无通知"; return titles.length ? titles.join(" ") : "";
}); });
const isNoticeScrolling = computed(() => noticeList.value.length > 1);
// <20>冽<EFBFBD><E586BD><EFBFBD><EFBFBD><EFBFBD>𡑒” // <20>冽<EFBFBD><E586BD><EFBFBD><EFBFBD><EFBFBD>𡑒”
const userPerms = computed(() => userStore.userInfo?.perms || []); const userPerms = computed(() => userStore.userInfo?.perms || []);
// 是否已登录 // <EFBFBD>臬炏撌脩蒈敶?const isLogged = computed(() => isLoggedIn());
const isLogged = computed(() => isLoggedIn());
// 璉<><E79289>交糓<E4BAA4>行<EFBFBD><E8A18C><EFBFBD><EFBFBD> // 璉<><E79289>交糓<E4BAA4>行<EFBFBD><E8A18C><EFBFBD><EFBFBD>
const hasPermission = (perm: string) => { const hasPermission = (perm: string) => {
@@ -214,8 +212,7 @@ const defaultNavList = computed(() => {
return result; return result;
}); });
// 快捷入口:已登录按权限过滤,未登录显示默认菜单 // 敹急㭘<EFBFBD>亙藁嚗𡁜歇<EFBFBD>餃<EFBFBD><EFBFBD>㗇<EFBFBD><EFBFBD>鞱<EFBFBD>皛歹<EFBFBD><EFBFBD>芰蒈敶閙遬蝷粹<EFBFBD>霈方<EFBFBD><EFBFBD>?const quickNavList = computed(() => {
const quickNavList = computed(() => {
if (!isLogged.value) { if (!isLogged.value) {
return defaultNavList.value; return defaultNavList.value;
} }
@@ -262,7 +259,6 @@ const chartOpts = ref({
function loadAppVersion() { function loadAppVersion() {
try { try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const p: any = (globalThis as any).plus; const p: any = (globalThis as any).plus;
if (p?.runtime?.version) { if (p?.runtime?.version) {
appVersion.value = `v${p.runtime.version}`; appVersion.value = `v${p.runtime.version}`;
@@ -279,7 +275,7 @@ async function loadNoticeData() {
return; return;
} }
try { try {
const { list } = await NoticeAPI.getMyNoticePage({ pageNum: 1, pageSize: 2 }); const { list } = await NoticeAPI.getMyNoticePage({ pageNum: 1, pageSize: 5 });
noticeList.value = list || []; noticeList.value = list || [];
} catch { } catch {
noticeList.value = []; noticeList.value = [];
@@ -296,7 +292,9 @@ async function loadVisitStatsData() {
async function loadVisitTrendData() { async function loadVisitTrendData() {
const endDate = dayjs().format("YYYY-MM-DD"); 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 { try {
const data: VisitTrendVO = await LogAPI.getVisitTrend({ startDate, endDate }); const data: VisitTrendVO = await LogAPI.getVisitTrend({ startDate, endDate });
@@ -304,8 +302,8 @@ async function loadVisitTrendData() {
JSON.stringify({ JSON.stringify({
categories: (data.dates || []).map((d) => dayjs(d).format("MM-DD")), categories: (data.dates || []).map((d) => dayjs(d).format("MM-DD")),
series: [ series: [
{ name: "访客数(UV)", data: data.uvList || [] }, { name: "霈踹恥<EFBFBD>?UV)", data: data.uvList || [] },
{ name: "浏览量(PV)", data: data.pvList || [] }, { name: "瘚讛<EFBFBD><EFBFBD>?PV)", data: data.pvList || [] },
], ],
}) })
); );
@@ -319,8 +317,7 @@ async function loadVisitTrendData() {
// ============================================================================ // ============================================================================
function handleNavClick(item: NavItem) { function handleNavClick(item: NavItem) {
// 未登录时先检查登录状态并跳转登录页 // <EFBFBD>芰蒈敶閙𧒄<EFBFBD><EFBFBD><EFBFBD><EFBFBD>亦蒈敶閧𠶖<EFBFBD><EFBFBD>僎頝唾蓮<EFBFBD>餃<EFBFBD>憿? if (!checkLogin()) {
if (!checkLogin()) {
return; return;
} }
router.push({ path: item.url }); router.push({ path: item.url });
@@ -376,29 +373,28 @@ onReady(() => {
.hero { .hero {
position: relative; position: relative;
} }
.hero::after { .hero::after {
content: "";
position: absolute; position: absolute;
left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
left: 0;
z-index: 1;
height: 120rpx; height: 120rpx;
pointer-events: none;
content: "";
background: linear-gradient( background: linear-gradient(
to bottom, to bottom,
rgba(245, 247, 250, 0) 0%, rgba(245, 247, 250, 0) 0%,
rgba(245, 247, 250, 0.85) 60%, rgba(245, 247, 250, 0.85) 60%,
rgba(245, 247, 250, 1) 100% rgba(245, 247, 250, 1) 100%
); );
pointer-events: none;
z-index: 1;
} }
:deep(.swiper-box) { :deep(.swiper-box) {
height: 420rpx; height: 420rpx;
overflow: hidden; overflow: hidden;
border-bottom-left-radius: 32rpx;
border-bottom-right-radius: 32rpx; border-bottom-right-radius: 32rpx;
border-bottom-left-radius: 32rpx;
} }
:deep(.swiper-box .wd-swiper__item), :deep(.swiper-box .wd-swiper__item),
@@ -407,8 +403,7 @@ onReady(() => {
} }
// ============================================================================ // ============================================================================
// 导航栏 // 撖潸⏛<EFBFBD>?// ============================================================================
// ============================================================================
.navbar { .navbar {
position: fixed; position: fixed;
@@ -461,16 +456,15 @@ onReady(() => {
.section--overlay { .section--overlay {
position: relative; position: relative;
z-index: 2; z-index: 2;
margin-top: -140rpx;
padding: 18rpx 8rpx; padding: 18rpx 8rpx;
margin-top: -140rpx;
background: var(--color-bg); background: var(--color-bg);
border-radius: 24rpx; border-radius: 24rpx;
box-shadow: 0 16rpx 36rpx rgba(0, 0, 0, 0.08); box-shadow: 0 16rpx 36rpx rgba(0, 0, 0, 0.08);
} }
// ============================================================================ // ============================================================================
// 快捷导航项 // 敹急㭘撖潸⏛憿?// ============================================================================
// ============================================================================
.nav-item { .nav-item {
display: flex; display: flex;
@@ -492,28 +486,27 @@ onReady(() => {
} }
// ============================================================================ // ============================================================================
// 通知栏 // <EFBFBD>𡁶䰻<EFBFBD>?// ============================================================================
// ============================================================================
.notice-bar { .notice-bar {
display: flex; display: flex;
align-items: center; align-items: center;
background: #fff;
border-radius: 16rpx;
padding: 24rpx 24rpx 24rpx 20rpx; padding: 24rpx 24rpx 24rpx 20rpx;
background: #fff;
border: 1rpx solid rgba(0, 0, 0, 0.04); border: 1rpx solid rgba(0, 0, 0, 0.04);
border-radius: 16rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04); box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
&__icon { &__icon {
flex-shrink: 0;
width: 48rpx;
height: 48rpx;
display: flex; display: flex;
flex-shrink: 0;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 48rpx;
height: 48rpx;
margin-right: 16rpx;
background: rgba(52, 209, 157, 0.1); background: rgba(52, 209, 157, 0.1);
border-radius: 12rpx; border-radius: 12rpx;
margin-right: 16rpx;
} }
&__content { &__content {
@@ -523,14 +516,35 @@ onReady(() => {
&__text { &__text {
font-size: 26rpx; font-size: 26rpx;
color: #333;
font-weight: 500; font-weight: 500;
display: -webkit-box; color: #333;
line-clamp: 1; white-space: nowrap;
-webkit-line-clamp: 1; }
-webkit-box-orient: vertical;
&__text--gap {
padding-left: 48rpx;
}
&__marquee {
width: 100%;
overflow: hidden; 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%);
}
} }
// ============================================================================ // ============================================================================
@@ -544,55 +558,49 @@ onReady(() => {
} }
.stat-card { .stat-card {
background: #fff; position: relative;
border-radius: 16rpx;
padding: 24rpx 20rpx;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04); padding: 24rpx 20rpx;
border: 1rpx solid rgba(0, 0, 0, 0.04);
position: relative;
overflow: hidden; 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);
// 背景图标容器
&__bg { &__bg {
position: absolute; position: absolute;
right: 12rpx; right: 12rpx;
bottom: 12rpx; bottom: 12rpx;
width: 70rpx; width: 70rpx;
height: 70rpx; height: 70rpx;
opacity: 0.08;
pointer-events: none; pointer-events: none;
opacity: 0.08;
} }
// SVG 装饰图形
&__icon { &__icon {
width: 100%; width: 100%;
height: 100%; height: 100%;
// 用户组图标 - 访客数
&--user { &--user {
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-image: url("/static/icons/visitor.svg");
background-size: contain;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: contain;
} }
// 图表图标 - 浏览量
&--eye { &--eye {
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-image: url("/static/icons/browser.svg");
background-size: contain;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: contain;
} }
// 盾牌徽章图标 - 版本
&--gear { &--gear {
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-image: url("/static/icons/setting.svg");
background-size: contain;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: contain;
} }
} }
// 彩色卡片变体
&--green { &--green {
background: linear-gradient(135deg, #f0fdf9 0%, #fff 100%); background: linear-gradient(135deg, #f0fdf9 0%, #fff 100%);
} }
@@ -606,18 +614,18 @@ onReady(() => {
} }
&__head { &__head {
position: relative;
z-index: 1;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-bottom: 16rpx; margin-bottom: 16rpx;
position: relative;
z-index: 1;
} }
&__label { &__label {
font-size: 24rpx; font-size: 24rpx;
color: #999;
font-weight: 500; font-weight: 500;
color: #999;
} }
&__dot { &__dot {
@@ -642,12 +650,12 @@ onReady(() => {
} }
&__num { &__num {
position: relative;
z-index: 1;
font-size: 48rpx; font-size: 48rpx;
font-weight: 700; font-weight: 700;
line-height: 1; line-height: 1;
letter-spacing: -1rpx; letter-spacing: -1rpx;
position: relative;
z-index: 1;
&--green { &--green {
color: #2ab789; color: #2ab789;
@@ -666,8 +674,8 @@ onReady(() => {
} }
&--full { &--full {
grid-column: 1 / -1;
flex-direction: row; flex-direction: row;
grid-column: 1 / -1;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 20rpx 20rpx; padding: 20rpx 20rpx;
@@ -682,10 +690,10 @@ onReady(() => {
} }
.stat-card__bg { .stat-card__bg {
width: 50rpx;
height: 50rpx;
right: 16rpx; right: 16rpx;
bottom: 50%; bottom: 50%;
width: 50rpx;
height: 50rpx;
transform: translateY(50%); transform: translateY(50%);
} }
} }

View File

@@ -9,14 +9,14 @@
<view class="login-nav" :style="{ paddingTop: `${statusBarHeight}px` }"> <view class="login-nav" :style="{ paddingTop: `${statusBarHeight}px` }">
<view class="login-nav__bar" :style="{ height: `${navBarHeight}px` }"> <view class="login-nav__bar" :style="{ height: `${navBarHeight}px` }">
<view class="login-nav__action" hover-class="login-nav__action--active" @click="handleBack"> <view class="login-nav__action" hover-class="login-nav__action--active" @click="handleBack">
<text class="login-nav__back-icon">‹</text> <text class="login-nav__back-icon"><EFBFBD>?/text>
</view> </view>
<view class="login-nav__title" /> <view class="login-nav__title" />
<view class="login-nav__placeholder" /> <view class="login-nav__placeholder" />
</view> </view>
</view> </view>
<!-- 主内容 --> <!-- 銝餃<EFBFBD>摰?-->
<view class="login-main" :style="{ paddingTop: `${statusBarHeight + navBarHeight + 4}px` }"> <view class="login-main" :style="{ paddingTop: `${statusBarHeight + navBarHeight + 4}px` }">
<!-- Logo --> <!-- Logo -->
<view class="login-logo"> <view class="login-logo">
@@ -34,7 +34,7 @@
<!-- 銵典<EFBFBD><EFBFBD>箏<EFBFBD> --> <!-- 銵典<EFBFBD><EFBFBD>箏<EFBFBD> -->
<view v-if="loginMode !== 'WECHAT'" class="form-area"> <view v-if="loginMode !== 'WECHAT'" class="form-area">
<!-- 用户名/手机号 --> <!-- <EFBFBD>冽<EFBFBD><EFBFBD>?<EFBFBD>𧢲㦤<EFBFBD>?-->
<view class="input-box"> <view class="input-box">
<wd-icon name="person" size="20" color="#9ca3af" /> <wd-icon name="person" size="20" color="#9ca3af" />
<input <input
@@ -52,7 +52,7 @@
v-model="formData.password" v-model="formData.password"
class="input-field" class="input-field"
:password="!showPassword" :password="!showPassword"
placeholder="请输入密码" placeholder="霂瑁<EFBFBD><EFBFBD>亙<EFBFBD><EFBFBD>?
:maxlength="50" :maxlength="50"
@confirm="handleLogin" @confirm="handleLogin"
/> />
@@ -80,7 +80,7 @@
/> />
</view> </view>
<!-- 验证码 --> <!-- 撉諹<EFBFBD><EFBFBD>?-->
<view v-else class="input-box"> <view v-else class="input-box">
<wd-icon name="shield" size="20" color="#9ca3af" /> <wd-icon name="shield" size="20" color="#9ca3af" />
<input <input
@@ -96,7 +96,7 @@
:class="smsCountdown > 0 ? 'sms-btn-disabled' : 'sms-btn-active'" :class="smsCountdown > 0 ? 'sms-btn-disabled' : 'sms-btn-active'"
@click="handleSendCode" @click="handleSendCode"
> >
{{ smsCountdown > 0 ? `${smsCountdown}s` : "获取验证码" }} {{ smsCountdown > 0 ? `${smsCountdown}s` : "<EFBFBD>瑕<EFBFBD>撉諹<EFBFBD><EFBFBD>? }}
</view> </view>
</view> </view>
@@ -112,16 +112,16 @@
:disabled="loading" :disabled="loading"
@click="handleLogin" @click="handleLogin"
> >
{{ loading ? "登录中..." : "登 录" }} {{ loading ? "<EFBFBD>餃<EFBFBD>銝?.." : "<EFBFBD>?敶? }}
</button> </button>
<!-- <EFBFBD><EFBFBD>揢<EFBFBD>餃<EFBFBD><EFBFBD>孵<EFBFBD> --> <!-- <EFBFBD><EFBFBD>揢<EFBFBD>餃<EFBFBD><EFBFBD>孵<EFBFBD> -->
<view class="switch-mode" @click="toggleLoginMode"> <view class="switch-mode" @click="toggleLoginMode">
<text class="switch-text"> <text class="switch-text">
{{ loginMode === "PASSWORD" ? "忘记密码?" : "记得密码?" }} {{ loginMode === "PASSWORD" ? "敹䁅扇撖<EFBFBD><EFBFBD>嚗? : "霈啣<EFBFBD>撖<EFBFBD><EFBFBD>嚗? }}
</text> </text>
<text class="switch-link"> <text class="switch-link">
{{ loginMode === "PASSWORD" ? "验证码登录" : "密码登录" }} {{ loginMode === "PASSWORD" ? "撉諹<EFBFBD><EFBFBD><EFBFBD>蒈敶? : "撖<EFBFBD><EFBFBD><EFBFBD>餃<EFBFBD>" }}
</text> </text>
</view> </view>
</view> </view>
@@ -135,8 +135,7 @@
@getphonenumber="handleWechatPhoneLogin" @getphonenumber="handleWechatPhoneLogin"
> >
<image class="btn-wechat-icon" src="/static/icons/weixin.png" mode="aspectFit" /> <image class="btn-wechat-icon" src="/static/icons/weixin.png" mode="aspectFit" />
微信一键登录 敺桐縑銝<EFBFBD><EFBFBD>桃蒈敶? </button>
</button>
<!-- <20>嗡<EFBFBD><E597A1>餃<EFBFBD><E9A483>孵<EFBFBD> --> <!-- <20>嗡<EFBFBD><E597A1>餃<EFBFBD><E9A483>孵<EFBFBD> -->
<view class="switch-mode" @click="loginMode = 'PASSWORD'"> <view class="switch-mode" @click="loginMode = 'PASSWORD'">
@@ -147,7 +146,7 @@
<!-- #endif --> <!-- #endif -->
<!-- #ifdef MP-WEIXIN --> <!-- #ifdef MP-WEIXIN -->
<!-- 分割线 --> <!-- <20><>𠧧蝥?-->
<view v-if="loginMode !== 'WECHAT'" class="divider"> <view v-if="loginMode !== 'WECHAT'" class="divider">
<view class="divider-line" /> <view class="divider-line" />
<text class="divider-text"><3E>嗡<EFBFBD><E597A1>餃<EFBFBD><E9A483>孵<EFBFBD></text> <text class="divider-text"><3E>嗡<EFBFBD><E597A1>餃<EFBFBD><E9A483>孵<EFBFBD></text>
@@ -167,14 +166,10 @@
<view class="agreement"> <view class="agreement">
<wd-checkbox v-model="isAgreePolicy" shape="square" size="16px"> <wd-checkbox v-model="isAgreePolicy" shape="square" size="16px">
<text class="agreement-text"> <text class="agreement-text">
我已阅读并同意 <EFBFBD>穃歇<EFBFBD><EFBFBD>粉撟嗅<EFBFBD><EFBFBD>? <text class="agreement-link" @click.stop="navigateToAgreement('user')">
<text class="agreement-link" @click.stop="navigateToAgreement('user')"> <20>羓鍂<E7BE93>瑕<EFBFBD>霈柴<E99C88>? </text>
《用户协议》 銝? <text class="agreement-link" @click.stop="navigateToAgreement('privacy')">
</text> <EFBFBD>𢠃<EFBFBD>蝘<EFBFBD>錇蝑硔<EFBFBD>? </text>
与
<text class="agreement-link" @click.stop="navigateToAgreement('privacy')">
《隐私政策》
</text>
</text> </text>
</wd-checkbox> </wd-checkbox>
</view> </view>
@@ -182,7 +177,7 @@
</view> </view>
</view> </view>
<!-- 绑定手机号弹窗 --> <!-- 蝏穃<E89D8F><E7A983>𧢲㦤<F0A7A2B2>瑕撕蝒?-->
<wd-popup <wd-popup
v-model="showBindMobilePopup" v-model="showBindMobilePopup"
position="bottom" position="bottom"
@@ -191,7 +186,7 @@
@close="resetBindForm" @close="resetBindForm"
> >
<view class="bind-popup"> <view class="bind-popup">
<text class="bind-title">绑定手机号</text> <text class="bind-title">蝏穃<E89D8F><E7A983>𧢲㦤<F0A7A2B2>?/text>
<view class="form-area"> <view class="form-area">
<view class="input-box"> <view class="input-box">
@@ -219,7 +214,7 @@
:class="bindSmsCountdown > 0 ? 'sms-btn-disabled' : 'sms-btn-active'" :class="bindSmsCountdown > 0 ? 'sms-btn-disabled' : 'sms-btn-active'"
@click="handleSendBindCode" @click="handleSendBindCode"
> >
{{ bindSmsCountdown > 0 ? `${bindSmsCountdown}s` : "获取验证码" }} {{ bindSmsCountdown > 0 ? `${bindSmsCountdown}s` : "<EFBFBD>瑕<EFBFBD>撉諹<EFBFBD><EFBFBD>? }}
</view> </view>
</view> </view>
@@ -229,7 +224,7 @@
</view> </view>
<button class="btn-bind" :disabled="bindLoading" @click="handleBindMobile"> <button class="btn-bind" :disabled="bindLoading" @click="handleBindMobile">
{{ bindLoading ? "绑定中..." : "确认绑定" }} {{ bindLoading ? "蝏穃<EFBFBD>銝?.." : "蝖株恕蝏穃<EFBFBD>" }}
</button> </button>
</view> </view>
</view> </view>
@@ -238,9 +233,8 @@
<!-- <EFBFBD>讛悅蝖株恕撘寧<EFBFBD> --> <!-- <EFBFBD>讛悅蝖株恕撘寧<EFBFBD> -->
<wd-message-box selector="policy-box"> <wd-message-box selector="policy-box">
<view class="text-center text-sm text-gray-500 leading-relaxed"> <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('user')">《用户协议》</text> <text class="text-blue-500" @click.stop="navigateToAgreement('privacy')"><EFBFBD>𢠃<EFBFBD>蝘<EFBFBD>錇蝑硔<EFBFBD>?/text>
<text class="text-blue-500" @click.stop="navigateToAgreement('privacy')">《隐私政策》</text>
</view> </view>
</wd-message-box> </wd-message-box>
@@ -268,8 +262,7 @@ const toast = useToast();
const message = useMessage("policy-box"); const message = useMessage("policy-box");
const userStore = useUserStore(); const userStore = useUserStore();
// 状态 // <EFBFBD>嗆<EFBFBD>?const loading = ref(false);
const loading = ref(false);
const showPassword = ref(false); const showPassword = ref(false);
const isAgreePolicy = ref(false); const isAgreePolicy = ref(false);
const loginMode = ref<"PASSWORD" | "SMS" | "WECHAT">("PASSWORD"); const loginMode = ref<"PASSWORD" | "SMS" | "WECHAT">("PASSWORD");
@@ -283,15 +276,13 @@ const formData = ref({
captchaCode: "", captchaCode: "",
}); });
// 图形验证码 // <EFBFBD>曉耦撉諹<EFBFBD><EFBFBD>?const captchaId = ref("");
const captchaId = ref("");
const captchaBase64 = ref(""); const captchaBase64 = ref("");
const captchaLoading = ref(false); const captchaLoading = ref(false);
const redirect = ref("/pages/index/index"); const redirect = ref("/pages/index/index");
// 绑定手机号相关 // 蝏穃<EFBFBD><EFBFBD>𧢲㦤<EFBFBD>瑞㮾<EFBFBD>?const showBindMobilePopup = ref(false);
const showBindMobilePopup = ref(false);
const bindLoading = ref(false); const bindLoading = ref(false);
const bindSmsCountdown = ref(0); const bindSmsCountdown = ref(0);
const bindSmsTimer = ref<ReturnType<typeof setInterval> | null>(null); const bindSmsTimer = ref<ReturnType<typeof setInterval> | null>(null);
@@ -305,8 +296,7 @@ const bindMobileForm = ref({
const pendingLoginAction = ref<"FORM" | "WECHAT_PHONE" | null>(null); const pendingLoginAction = ref<"FORM" | "WECHAT_PHONE" | null>(null);
const pendingWechatPhoneCode = ref<string>(""); const pendingWechatPhoneCode = ref<string>("");
// 获取图形验证码 // <EFBFBD>瑕<EFBFBD><EFBFBD>曉耦撉諹<EFBFBD><EFBFBD>?const fetchCaptcha = async () => {
const fetchCaptcha = async () => {
if (captchaLoading.value) return; if (captchaLoading.value) return;
try { try {
captchaLoading.value = true; captchaLoading.value = true;
@@ -315,17 +305,16 @@ const fetchCaptcha = async () => {
captchaId.value = res.captchaId; captchaId.value = res.captchaId;
captchaBase64.value = res.captchaBase64; captchaBase64.value = res.captchaBase64;
} catch (e) { } catch (e) {
console.error("获取验证码失败", e); console.error("<EFBFBD>瑕<EFBFBD>撉諹<EFBFBD><EFBFBD><EFBFBD>仃韐?, e);
} finally { } finally {
captchaLoading.value = false; captchaLoading.value = false;
} }
}; };
// 计算属性 // 霈∠<EFBFBD>撅墧<EFBFBD>?const loginModeDesc = computed(() => {
const loginModeDesc = computed(() => {
const modeMap = { const modeMap = {
PASSWORD: "雿輻鍂韐血噡撖<EFBFBD><EFBFBD><EFBFBD>餃<EFBFBD>", PASSWORD: "雿輻鍂韐血噡撖<EFBFBD><EFBFBD><EFBFBD>餃<EFBFBD>",
SMS: "使用手机验证码登录", SMS: "雿輻鍂<EFBFBD>𧢲㦤撉諹<EFBFBD><EFBFBD><EFBFBD>蒈敶?,
WECHAT: "雿輻鍂敺桐縑敹急㭘<E680A5>餃<EFBFBD>", WECHAT: "雿輻鍂敺桐縑敹急㭘<E680A5>餃<EFBFBD>",
}; };
return modeMap[loginMode.value]; return modeMap[loginMode.value];
@@ -362,13 +351,11 @@ onLoad((options: any) => {
// #endif // #endif
// #ifndef MP-WEIXIN // #ifndef MP-WEIXIN
// 非微信环境强制使用密码登录 // <EFBFBD>𧼮凝靽∠㴓憓<EFBFBD>撩<EFBFBD>嗡蝙<EFBFBD>典<EFBFBD><EFBFBD><EFBFBD>蒈敶? if (loginMode.value === "WECHAT") {
if (loginMode.value === "WECHAT") {
loginMode.value = "PASSWORD"; loginMode.value = "PASSWORD";
} }
// #endif // #endif
// 获取图形验证码 // <EFBFBD>瑕<EFBFBD><EFBFBD>曉耦撉諹<EFBFBD><EFBFBD>? fetchCaptcha();
fetchCaptcha();
}); });
onShow(() => { onShow(() => {
@@ -425,9 +412,9 @@ const openPolicyDialog = (action: "FORM" | "WECHAT_PHONE", phoneCode: string = "
async function doFormLogin() { async function doFormLogin() {
if (!canSubmit.value) { if (!canSubmit.value) {
if (loginMode.value === "PASSWORD") { if (loginMode.value === "PASSWORD") {
toast.error("请输入用户名和密码"); toast.error("霂瑁<EFBFBD><EFBFBD>亦鍂<EFBFBD>瑕<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>?);
} else if (loginMode.value === "SMS") { } else if (loginMode.value === "SMS") {
toast.error("请输入正确的手机号和验证码"); toast.error("霂瑁<EFBFBD><EFBFBD>交迤蝖桃<EFBFBD><EFBFBD>𧢲㦤<EFBFBD>瑕<EFBFBD>撉諹<EFBFBD><EFBFBD>?);
} }
return; return;
} }
@@ -456,8 +443,7 @@ async function doFormLogin() {
setTimeout(() => uni.reLaunch({ url: redirect.value }), 800); setTimeout(() => uni.reLaunch({ url: redirect.value }), 800);
} catch (error: any) { } catch (error: any) {
toast.error(error?.message || "<22>餃<EFBFBD>憭梯揖"); toast.error(error?.message || "<22>餃<EFBFBD>憭梯揖");
// 登录失败刷新验证码 // <EFBFBD>餃<EFBFBD>憭梯揖<EFBFBD>瑟鰵撉諹<EFBFBD><EFBFBD>? if (loginMode.value === "PASSWORD") {
if (loginMode.value === "PASSWORD") {
fetchCaptcha(); fetchCaptcha();
} }
} finally { } finally {
@@ -481,16 +467,16 @@ const handleSendCode = async () => {
return; return;
} }
if (!isValidMobile(mobile)) { if (!isValidMobile(mobile)) {
toast.error("请输入正确的手机号"); toast.error("霂瑁<EFBFBD><EFBFBD>交迤蝖桃<EFBFBD><EFBFBD>𧢲㦤<EFBFBD>?);
return; return;
} }
try { try {
await AuthAPI.sendSmsLoginCode(mobile); await AuthAPI.sendSmsLoginCode(mobile);
toast.success("验证码已发送"); toast.success("撉諹<EFBFBD><EFBFBD><EFBFBD>歇<EFBFBD>煾<EFBFBD>?);
startSmsCountdown(smsCountdown, smsTimer); startSmsCountdown(smsCountdown, smsTimer);
} catch (error: any) { } catch (error: any) {
toast.error(error?.message || "发送失败"); toast.error(error?.message || "<EFBFBD>煾<EFBFBD><EFBFBD>仃韐?);
} }
}; };
@@ -574,16 +560,16 @@ const handleSendBindCode = async () => {
if (bindSmsCountdown.value > 0) return; if (bindSmsCountdown.value > 0) return;
const mobile = bindMobileForm.value.mobile.trim(); const mobile = bindMobileForm.value.mobile.trim();
if (!isValidMobile(mobile)) { if (!isValidMobile(mobile)) {
toast.error("请输入正确的手机号"); toast.error("霂瑁<EFBFBD><EFBFBD>交迤蝖桃<EFBFBD><EFBFBD>𧢲㦤<EFBFBD>?);
return; return;
} }
try { try {
await AuthAPI.sendSmsLoginCode(mobile); await AuthAPI.sendSmsLoginCode(mobile);
toast.success("验证码已发送"); toast.success("撉諹<EFBFBD><EFBFBD><EFBFBD>歇<EFBFBD>煾<EFBFBD>?);
startSmsCountdown(bindSmsCountdown, bindSmsTimer); startSmsCountdown(bindSmsCountdown, bindSmsTimer);
} catch (error: any) { } catch (error: any) {
toast.error(error?.message || "发送失败"); toast.error(error?.message || "<EFBFBD>煾<EFBFBD><EFBFBD>仃韐?);
} }
}; };
@@ -600,7 +586,7 @@ const handleBindMobile = async () => {
if (bindLoading.value) return; if (bindLoading.value) return;
const { mobile, code } = bindMobileForm.value; const { mobile, code } = bindMobileForm.value;
if (!isValidMobile(mobile)) { if (!isValidMobile(mobile)) {
toast.error("请输入正确的手机号"); toast.error("霂瑁<EFBFBD><EFBFBD>交迤蝖桃<EFBFBD><EFBFBD>𧢲㦤<EFBFBD>?);
return; return;
} }
if (!code.trim()) { if (!code.trim()) {
@@ -746,8 +732,7 @@ const handleBack = () => {
letter-spacing: 0.08em; letter-spacing: 0.08em;
} }
// 主内容 // 銝餃<EFBFBD>摰?.login-main {
.login-main {
position: relative; position: relative;
z-index: 10; z-index: 10;
display: flex; display: flex;
@@ -834,8 +819,7 @@ const handleBack = () => {
} }
} }
// 输入框容器 // 颲枏<EFBFBD>獢<EFBFBD>捆<EFBFBD>?.input-box {
.input-box {
display: flex; display: flex;
align-items: center; align-items: center;
height: 48px; height: 48px;
@@ -868,8 +852,7 @@ const handleBack = () => {
padding: 8px; padding: 8px;
} }
// 主按钮 // 銝餅<EFBFBD><EFBFBD>?.btn-primary {
.btn-primary {
position: relative; position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -967,8 +950,7 @@ const handleBack = () => {
} }
} }
// 验证码按钮 // 撉諹<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>?.sms-btn {
.sms-btn {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -1035,8 +1017,7 @@ const handleBack = () => {
border-bottom-color: #60a5fa; border-bottom-color: #60a5fa;
} }
// 分割线 // <EFBFBD><EFBFBD>𠧧蝥?.divider {
.divider {
display: flex; display: flex;
align-items: center; align-items: center;
margin-top: 24px; margin-top: 24px;
@@ -1094,8 +1075,7 @@ const handleBack = () => {
color: #2563eb; color: #2563eb;
} }
// 绑定手机号弹窗 // 蝏穃<EFBFBD><EFBFBD>𧢲㦤<EFBFBD>瑕撕蝒?.bind-popup {
.bind-popup {
padding: 24px; padding: 24px;
} }

View File

@@ -1,5 +1,5 @@
<template> <template>
<view class="page-container dark:text-[var(--wot-color-text)]"> <view class="page dark:text-[var(--wot-color-text)]">
<wd-card> <wd-card>
<view class="about-header"> <view class="about-header">
<wd-img <wd-img

View File

@@ -1,5 +1,5 @@
<template> <template>
<view class="page-container dark:text-[var(--wot-color-text)]"> <view class="page dark:text-[var(--wot-color-text)]">
<wd-card> <wd-card>
<wd-cell-group border> <wd-cell-group border>
<wd-cell <wd-cell

View File

@@ -1,5 +1,5 @@
<template> <template>
<view class="page-container dark:text-[var(--wot-color-text)]"> <view class="page dark:text-[var(--wot-color-text)]">
<wd-text size="small">选填,最多上传3张图片</wd-text> <wd-text size="small">选填,最多上传3张图片</wd-text>
<wd-form ref="formRef" :model="formData" :rules="rules"> <wd-form ref="formRef" :model="formData" :rules="rules">
<wd-form-item label="问题类型" prop="feedbackType"> <wd-form-item label="问题类型" prop="feedbackType">

View File

@@ -1,5 +1,5 @@
<template> <template>
<view class="page-container page-container--gradient dark:text-[var(--wot-color-text)]"> <view class="page dark:text-[var(--wot-color-text)]">
<view class="mine-hero" :style="{ paddingTop: `${navbar.totalHeight.value}px` }"> <view class="mine-hero" :style="{ paddingTop: `${navbar.totalHeight.value}px` }">
<!-- 蓝色背景 --> <!-- 蓝色背景 -->
<view class="mine-hero__bg" :style="{ background: headerBackground }" /> <view class="mine-hero__bg" :style="{ background: headerBackground }" />
@@ -509,8 +509,8 @@ const helpItems = computed<ActionItem[]>(() => [
// ... // ...
.mine-hero { .mine-hero {
position: relative; position: relative;
overflow: visible;
padding-bottom: 24rpx; padding-bottom: 24rpx;
overflow: visible;
} }
.mine-hero__bg { .mine-hero__bg {
@@ -558,13 +558,13 @@ const helpItems = computed<ActionItem[]>(() => [
padding: 28rpx 28rpx; padding: 28rpx 28rpx;
overflow: hidden; overflow: hidden;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.22) 0%, rgba(255, 255, 255, 0.12) 100%); background: linear-gradient(135deg, rgba(255, 255, 255, 0.22) 0%, rgba(255, 255, 255, 0.12) 100%);
backdrop-filter: blur(20px);
border: 1rpx solid rgba(255, 255, 255, 0.35); border: 1rpx solid rgba(255, 255, 255, 0.35);
border-radius: 28rpx; border-radius: 28rpx;
box-shadow: box-shadow:
0 8rpx 32rpx rgba(0, 0, 0, 0.08), 0 8rpx 32rpx rgba(0, 0, 0, 0.08),
0 2rpx 8rpx rgba(0, 0, 0, 0.04), 0 2rpx 8rpx rgba(0, 0, 0, 0.04),
inset 0 1rpx 0 rgba(255, 255, 255, 0.25); inset 0 1rpx 0 rgba(255, 255, 255, 0.25);
backdrop-filter: blur(20px);
} }
.profile-card::before { .profile-card::before {
@@ -573,16 +573,16 @@ const helpItems = computed<ActionItem[]>(() => [
right: -20%; right: -20%;
width: 200rpx; width: 200rpx;
height: 200rpx; height: 200rpx;
pointer-events: none;
content: ""; content: "";
background: radial-gradient(circle, rgba(255, 255, 255, 0.25) 0%, transparent 70%); background: radial-gradient(circle, rgba(255, 255, 255, 0.25) 0%, transparent 70%);
border-radius: 50%; border-radius: 50%;
pointer-events: none;
} }
.profile-card__header { .profile-card__header {
display: flex; display: flex;
align-items: center;
gap: 28rpx; gap: 28rpx;
align-items: center;
width: 100%; width: 100%;
} }
@@ -592,11 +592,11 @@ const helpItems = computed<ActionItem[]>(() => [
} }
.profile-card__avatar { .profile-card__avatar {
flex-shrink: 0;
width: 120rpx; width: 120rpx;
height: 120rpx; height: 120rpx;
border: 3rpx solid rgba(255, 255, 255, 0.9); border: 3rpx solid rgba(255, 255, 255, 0.9);
border-radius: 50%; border-radius: 50%;
flex-shrink: 0;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1); box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
} }
@@ -625,11 +625,11 @@ const helpItems = computed<ActionItem[]>(() => [
.profile-card__actions { .profile-card__actions {
display: flex; display: flex;
align-items: center;
gap: 10rpx;
flex-shrink: 0; flex-shrink: 0;
margin-left: auto; gap: 10rpx;
align-items: center;
margin-right: -10rpx; margin-right: -10rpx;
margin-left: auto;
} }
.profile-card__action-btn { .profile-card__action-btn {
@@ -640,15 +640,18 @@ const helpItems = computed<ActionItem[]>(() => [
width: 52rpx; width: 52rpx;
height: 52rpx; height: 52rpx;
background: rgba(255, 255, 255, 0.14); background: rgba(255, 255, 255, 0.14);
backdrop-filter: blur(10px);
border: 1rpx solid rgba(255, 255, 255, 0.22); border: 1rpx solid rgba(255, 255, 255, 0.22);
border-radius: 999rpx; border-radius: 999rpx;
backdrop-filter: blur(10px);
} }
.profile-card__notify-badge { .profile-card__notify-badge {
position: absolute; position: absolute;
top: -8rpx; top: -8rpx;
right: -6rpx; right: -6rpx;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 30rpx; min-width: 30rpx;
height: 30rpx; height: 30rpx;
padding: 0 8rpx; padding: 0 8rpx;
@@ -657,9 +660,6 @@ const helpItems = computed<ActionItem[]>(() => [
background: #ef4444; background: #ef4444;
border: 2rpx solid rgba(255, 255, 255, 0.95); border: 2rpx solid rgba(255, 255, 255, 0.95);
border-radius: 999rpx; border-radius: 999rpx;
display: inline-flex;
align-items: center;
justify-content: center;
} }
.gender-icon--male { .gender-icon--male {
@@ -676,8 +676,8 @@ const helpItems = computed<ActionItem[]>(() => [
} }
.profile-card__top { .profile-card__top {
align-items: center;
gap: 16rpx; gap: 16rpx;
align-items: center;
margin-bottom: 18rpx; margin-bottom: 18rpx;
} }
@@ -703,14 +703,14 @@ const helpItems = computed<ActionItem[]>(() => [
.profile-card__desc { .profile-card__desc {
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
margin-top: 12rpx; margin-top: 12rpx;
overflow: hidden;
font-size: 24rpx; font-size: 24rpx;
line-height: 1.6; line-height: 1.6;
color: rgba(255, 255, 255, 0.84); color: rgba(255, 255, 255, 0.84);
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
} }
.profile-card__hint { .profile-card__hint {
@@ -741,25 +741,25 @@ const helpItems = computed<ActionItem[]>(() => [
} }
.quick-cards { .quick-cards {
position: relative;
z-index: 2;
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
gap: 20rpx; gap: 20rpx;
margin-top: 24rpx; margin-top: 24rpx;
margin-bottom: 0; margin-bottom: 0;
position: relative;
z-index: 2;
} }
.community-card { .community-card {
position: relative; position: relative;
display: flex; display: flex;
align-items: center;
gap: 24rpx; gap: 24rpx;
align-items: center;
padding: 36rpx; padding: 36rpx;
overflow: hidden;
background: transparent; background: transparent;
border-radius: 32rpx; border-radius: 32rpx;
box-shadow: 0 10rpx 30rpx rgba(15, 23, 42, 0.06); box-shadow: 0 10rpx 30rpx rgba(15, 23, 42, 0.06);
overflow: hidden;
} }
.community-card__bg { .community-card__bg {
@@ -768,8 +768,8 @@ const helpItems = computed<ActionItem[]>(() => [
z-index: 0; z-index: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
opacity: 1;
pointer-events: none; pointer-events: none;
opacity: 1;
} }
.community-card__mask { .community-card__mask {
@@ -799,13 +799,13 @@ const helpItems = computed<ActionItem[]>(() => [
.community-card__logo { .community-card__logo {
position: relative; position: relative;
z-index: 2; z-index: 2;
box-sizing: border-box;
width: 84rpx; width: 84rpx;
height: 84rpx; height: 84rpx;
padding: 10rpx; padding: 10rpx;
box-sizing: border-box;
border-radius: 20rpx;
background: rgba(255, 255, 255, 0.96); background: rgba(255, 255, 255, 0.96);
border: 1rpx solid rgba(148, 163, 184, 0.22); border: 1rpx solid rgba(148, 163, 184, 0.22);
border-radius: 20rpx;
box-shadow: 0 6rpx 18rpx rgba(15, 23, 42, 0.06); box-shadow: 0 6rpx 18rpx rgba(15, 23, 42, 0.06);
} }
@@ -830,8 +830,8 @@ const helpItems = computed<ActionItem[]>(() => [
.community-card__title-row { .community-card__title-row {
display: flex; display: flex;
align-items: center;
gap: 16rpx; gap: 16rpx;
align-items: center;
} }
.community-card__title { .community-card__title {
@@ -849,15 +849,15 @@ const helpItems = computed<ActionItem[]>(() => [
.community-card__desc { .community-card__desc {
display: block; display: block;
margin-top: 10rpx;
font-size: 24rpx;
color: var(--wot-color-text-secondary, #64748b);
line-height: 1.55;
display: -webkit-box; display: -webkit-box;
margin-top: 10rpx;
overflow: hidden;
font-size: 24rpx;
line-height: 1.55;
color: var(--wot-color-text-secondary, #64748b);
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
line-clamp: 2; line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden;
} }
.section-card { .section-card {
@@ -873,16 +873,16 @@ const helpItems = computed<ActionItem[]>(() => [
.section-title { .section-title {
display: block; display: block;
margin-bottom: 18rpx;
font-size: 26rpx; font-size: 26rpx;
font-weight: 600; font-weight: 600;
color: var(--wot-color-text, #2f3a4a); color: var(--wot-color-text, #2f3a4a);
margin-bottom: 18rpx;
} }
.quick-card { .quick-card {
display: flex; display: flex;
align-items: center;
gap: 20rpx; gap: 20rpx;
align-items: center;
padding: 28rpx 24rpx; padding: 28rpx 24rpx;
background: var(--wot-color-white, #fff); background: var(--wot-color-white, #fff);
border-radius: 24rpx; border-radius: 24rpx;
@@ -891,17 +891,17 @@ const helpItems = computed<ActionItem[]>(() => [
} }
.quick-card:active { .quick-card:active {
transform: scale(0.96);
box-shadow: 0 4rpx 12rpx rgba(15, 23, 42, 0.06); box-shadow: 0 4rpx 12rpx rgba(15, 23, 42, 0.06);
transform: scale(0.96);
} }
.quick-card__icon { .quick-card__icon {
width: 84rpx;
height: 84rpx;
border-radius: 22rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 84rpx;
height: 84rpx;
border-radius: 22rpx;
} }
.quick-card__body { .quick-card__body {
@@ -911,22 +911,22 @@ const helpItems = computed<ActionItem[]>(() => [
.quick-card__title { .quick-card__title {
display: block; display: block;
overflow: hidden;
font-size: 28rpx; font-size: 28rpx;
font-weight: 500; font-weight: 500;
color: var(--wot-color-text, #2f3a4a);
line-height: 1.35; line-height: 1.35;
overflow: hidden; color: var(--wot-color-text, #2f3a4a);
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.quick-card__desc { .quick-card__desc {
display: block; display: block;
font-size: 22rpx;
color: var(--wot-color-text-secondary, #64748b);
line-height: 1.6;
margin-top: 8rpx; margin-top: 8rpx;
overflow: hidden; overflow: hidden;
font-size: 22rpx;
line-height: 1.6;
color: var(--wot-color-text-secondary, #64748b);
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
@@ -943,33 +943,33 @@ const helpItems = computed<ActionItem[]>(() => [
.mine-body { .mine-body {
position: relative; position: relative;
z-index: 0; z-index: 0;
padding: 0 28rpx calc(env(safe-area-inset-bottom) + 40rpx);
margin-top: 0; margin-top: 0;
background: var(--wot-color-bg, #f8fafc); background: var(--wot-color-bg, #f8fafc);
border-top-left-radius: 48rpx; border-top-left-radius: 48rpx;
border-top-right-radius: 48rpx; border-top-right-radius: 48rpx;
padding: 0 28rpx calc(env(safe-area-inset-bottom) + 40rpx);
} }
.menu-list { .menu-list {
margin-top: 24rpx;
padding: 0 28rpx; padding: 0 28rpx;
margin-top: 24rpx;
overflow: hidden;
background: var(--wot-color-white, #fff); background: var(--wot-color-white, #fff);
border-radius: 32rpx; border-radius: 32rpx;
box-shadow: 0 16rpx 40rpx rgba(15, 23, 42, 0.05); box-shadow: 0 16rpx 40rpx rgba(15, 23, 42, 0.05);
overflow: hidden;
} }
.menu-list--flat { .menu-list--flat {
margin-top: 0;
padding: 0; padding: 0;
margin-top: 0;
background: transparent; background: transparent;
border-radius: 0; border-radius: 0;
box-shadow: none; box-shadow: none;
} }
.menu-row { .menu-row {
align-items: center;
gap: 20rpx; gap: 20rpx;
align-items: center;
padding: 24rpx 20rpx; padding: 24rpx 20rpx;
background: transparent; background: transparent;
} }
@@ -979,9 +979,9 @@ const helpItems = computed<ActionItem[]>(() => [
} }
.menu-row__icon { .menu-row__icon {
flex-shrink: 0;
width: 76rpx; width: 76rpx;
height: 76rpx; height: 76rpx;
flex-shrink: 0;
} }
.menu-row__main { .menu-row__main {

View File

@@ -1,5 +1,5 @@
<template> <template>
<view class="page-container dark:text-[var(--wot-color-text)]"> <view class="page dark:text-[var(--wot-color-text)]">
<view class="page-content"> <view class="page-content">
<view class="header"> <view class="header">
<image class="logo" src="/static/logo.png" mode="aspectFill" /> <image class="logo" src="/static/logo.png" mode="aspectFill" />
@@ -45,8 +45,8 @@
.header { .header {
display: flex; display: flex;
align-items: center;
gap: 20rpx; gap: 20rpx;
align-items: center;
padding: 28rpx; padding: 28rpx;
background: #fff; background: #fff;
border-radius: 20rpx; border-radius: 20rpx;
@@ -77,8 +77,8 @@
} }
.group-tip { .group-tip {
margin-top: 20rpx;
padding: 20rpx; padding: 20rpx;
margin-top: 20rpx;
font-size: 26rpx; font-size: 26rpx;
line-height: 1.6; line-height: 1.6;
color: #374151; color: #374151;
@@ -87,8 +87,8 @@
} }
.qr-card { .qr-card {
margin-top: 24rpx;
padding: 28rpx; padding: 28rpx;
margin-top: 24rpx;
background: #fff; background: #fff;
border-radius: 20rpx; border-radius: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04); box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
@@ -101,8 +101,8 @@
.qr-tip { .qr-tip {
margin-top: 20rpx; margin-top: 20rpx;
text-align: center;
font-size: 26rpx; font-size: 26rpx;
color: #6b7280; color: #6b7280;
text-align: center;
} }
</style> </style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<view class="page-container dark:text-[var(--wot-color-text)]"> <view class="page dark:text-[var(--wot-color-text)]">
<!-- 头部标题 --> <!-- 头部标题 -->
<view class="header"> <view class="header">
<view class="title">完善个人信息</view> <view class="title">完善个人信息</view>

View File

@@ -1,5 +1,5 @@
<template> <template>
<view class="page-container page-container--auto dark:text-[var(--wot-color-text)]"> <view class="page page--auto dark:text-[var(--wot-color-text)]">
<view class="profile-content"> <view class="profile-content">
<view class="pt-20rpx"> <view class="pt-20rpx">
<wd-card v-if="userProfile"> <wd-card v-if="userProfile">
@@ -184,8 +184,6 @@ function handleTouchMove(event: TouchEvent) {
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.avatar-cell { .avatar-cell {
:deep(.wd-cell__body) { :deep(.wd-cell__body) {
align-items: center; align-items: center;

View File

@@ -1,5 +1,5 @@
<template> <template>
<view class="page-container dark:text-[var(--wot-color-text)]"> <view class="page dark:text-[var(--wot-color-text)]">
<wd-collapse v-model="activeNames" accordion> <wd-collapse v-model="activeNames" accordion>
<wd-collapse-item <wd-collapse-item
v-for="(section, index) in agreementContent" v-for="(section, index) in agreementContent"

View File

@@ -1,5 +1,5 @@
<template> <template>
<view class="page-container dark:text-[var(--wot-color-text)]"> <view class="page dark:text-[var(--wot-color-text)]">
<wd-cell-group> <wd-cell-group>
<wd-cell title="主题设置" icon="setting1" is-link @click="navigateToTheme" /> <wd-cell title="主题设置" icon="setting1" is-link @click="navigateToTheme" />
<wd-cell title="用户协议" icon="user" is-link @click="navigateToUserAgreement" /> <wd-cell title="用户协议" icon="user" is-link @click="navigateToUserAgreement" />

View File

@@ -1,5 +1,5 @@
<template> <template>
<view class="page-container dark:text-[var(--wot-color-text)]"> <view class="page dark:text-[var(--wot-color-text)]">
<!-- 网络状态展示 --> <!-- 网络状态展示 -->
<wd-card> <wd-card>
<wd-cell-group border> <wd-cell-group border>

View File

@@ -1,5 +1,5 @@
<template> <template>
<view class="page-container dark:text-[var(--wot-color-text)]"> <view class="page dark:text-[var(--wot-color-text)]">
<wd-collapse v-model="activeNames" accordion> <wd-collapse v-model="activeNames" accordion>
<wd-collapse-item <wd-collapse-item
v-for="(section, index) in privacyContent" v-for="(section, index) in privacyContent"

View File

@@ -1,5 +1,5 @@
<template> <template>
<view class="page-container dark:text-[var(--wot-color-text)]"> <view class="page dark:text-[var(--wot-color-text)]">
<!-- 暗黑模式设置 --> <!-- 暗黑模式设置 -->
<wd-card custom-class="mx-30rpx my-20rpx"> <wd-card custom-class="mx-30rpx my-20rpx">
<template #header> <template #header>

View File

@@ -1,5 +1,5 @@
<template> <template>
<view class="work"> <view class="page">
<!-- 筛选 --> <!-- 筛选 -->
<wd-drop-menu close-on-click-modal class="mb-20rpx mr-20rpx ml-20rpx"> <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"> <wd-drop-menu-item ref="dropMenu" title="筛选" icon="filter" icon-size="18px">

View File

@@ -1,5 +1,27 @@
<template> <template>
<view class="dept-page"> <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="search-bar"> <view class="search-bar">
<wd-search <wd-search
@@ -11,28 +33,39 @@
</view> </view>
<!-- 部门列表 --> <!-- 部门列表 -->
<scroll-view class="dept-list" scroll-y> <scroll-view
class="dept-list"
scroll-y
refresher-enabled
:refresher-triggered="isRefreshing"
@refresherrefresh="onRefresh"
>
<view v-if="filteredDeptList.length > 0" class="dept-tree"> <view v-if="filteredDeptList.length > 0" class="dept-tree">
<template v-for="dept in filteredDeptList" :key="dept.id"> <template v-for="dept in filteredDeptList" :key="dept.id">
<dept-item <dept-item
:dept="dept" :dept="dept"
:level="0" :level="0"
@click="handleDeptClick" @click="handleDeptClick"
@edit="handleEditDept"
@delete="handleDeleteDept"
/> />
</template> </template>
</view> </view>
<!-- 空状态 --> <!-- 空状态 -->
<view v-else class="empty-state"> <view v-else class="empty-state">
<wd-icon name="folder-open" size="48" color="#d1d5db" /> <view class="empty-state__icon-wrap">
<text class="empty-state__text">暂无部门数据</text> <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>
</view> </view>
</scroll-view> </scroll-view>
<!-- 底部操作栏 --> <!-- 底部操作栏 -->
<view class="action-bar"> <view class="action-bar">
<wd-button type="primary" block @click="handleAddDept"> <wd-button type="primary" block @click="handleAddDept">
<wd-icon name="add" size="16" color="#fff" /> <wd-icon name="add" size="18" color="#fff" />
<text>新增部门</text> <text>新增部门</text>
</wd-button> </wd-button>
</view> </view>
@@ -44,15 +77,18 @@
* 部门管理页面 * 部门管理页面
* *
* 功能说明: * 功能说明:
* - 展示部门树形结构 * - 部门树形结构展示
* - 支持搜索部门 * - 部门统计概览
* - 新增/编辑/删除部门 * - 搜索过滤
* - 下拉刷新
* - 滑动操作(编辑/删除)
* - 新增部门
* *
* 技术要点: * 技术要点:
* - 使用 BEM 命名规范 * - 使用 BEM 命名规范
* - 支持暗黑模式 * - 支持暗黑模式
* - 使用 CSS 变量 * - CSS 变量
* - 递归组件实现树形结构 * - 递归组件
*/ */
// ============================================================================ // ============================================================================
@@ -60,14 +96,14 @@
// ============================================================================ // ============================================================================
interface DeptNode { interface DeptNode {
id: string id: string;;
label: string label: string;;
userCount?: number userCount?: number;;
children?: DeptNode[] children?: DeptNode[];;
} }
// ============================================================================ // ============================================================================
// 子组件 - 部门项(支持递归) // 子组件 - 部门项(支持递归 + 滑动操作)
// ============================================================================ // ============================================================================
const DeptItem = defineComponent({ const DeptItem = defineComponent({
@@ -76,55 +112,91 @@ const DeptItem = defineComponent({
dept: { type: Object as PropType<DeptNode>, required: true }, dept: { type: Object as PropType<DeptNode>, required: true },
level: { type: Number, default: 0 }, level: { type: Number, default: 0 },
}, },
emits: ["click"], emits: ["click", "edit", "delete"],
setup(props, { emit }) { setup(props, { emit }) {
const isExpanded = ref(false) const isExpanded = ref(false);;
const hasChildren = computed(() => props.dept.children && props.dept.children.length > 0) const isSwipeOpen = ref(false);;
const hasChildren = computed(() => props.dept.children && props.dept.children.length > 0);;
function toggleExpand() { function toggleExpand() {
if (hasChildren.value) { if (hasChildren.value) {
isExpanded.value = !isExpanded.value isExpanded.value = !isExpanded.value;;
} }
} }
function handleClick() { function handleClick() {
emit("click", props.dept) emit("click", props.dept);;
}
function handleEdit() {
emit("edit", props.dept);;
}
function handleDelete() {
emit("delete", props.dept);;
}
function closeSwipe() {
isSwipeOpen.value = false;;
} }
return { return {
isExpanded, isExpanded,
isSwipeOpen,
hasChildren, hasChildren,
toggleExpand, toggleExpand,
handleClick, handleClick,
} handleEdit,
handleDelete,
closeSwipe,
};;
}, },
template: ` template: `
<view class="dept-node"> <view class="dept-node">
<view <view class="dept-node__swipe-wrap">
class="dept-node__item" <view class="dept-node__actions">
:class="{ 'dept-node__item--has-children': hasChildren }" <view class="dept-node__action dept-node__action--edit" @click="handleEdit">
:style="{ paddingLeft: level * 32 + 16 + 'rpx' }" <wd-icon name="edit" size="16" color="#fff" />
> <text>编辑</text>
<view class="dept-node__expand" @click="toggleExpand"> </view>
<wd-icon <view class="dept-node__action dept-node__action--delete" @click="handleDelete">
v-if="hasChildren" <wd-icon name="delete" size="16" color="#fff" />
name="arrow-right" <text>删除</text>
size="14" </view>
class="dept-node__arrow"
:class="{ 'dept-node__arrow--expanded': isExpanded }"
/>
</view> </view>
<view class="dept-node__content" @click="handleClick"> <view
<wd-icon class="dept-node__item"
name="folder" :class="{ 'dept-node__item--has-children': hasChildren }"
size="18" :style="{ paddingLeft: level * 24 + 16 + 'rpx' }"
:color="hasChildren ? 'var(--color-primary)' : 'var(--color-text-secondary)'" @click="handleClick"
/> >
<text class="dept-node__name">{{ dept.label }}</text> <view class="dept-node__expand" @click.stop="toggleExpand">
</view> <wd-icon
<view class="dept-node__meta" @click="handleClick"> v-if="hasChildren"
<text class="dept-node__count">{{ dept.userCount || 0 }} 人</text> name="arrow-right"
<wd-icon name="arrow-right" size="14" color="var(--color-text-secondary)" /> 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> </view>
</view> </view>
@@ -136,19 +208,22 @@ const DeptItem = defineComponent({
:dept="child" :dept="child"
:level="level + 1" :level="level + 1"
@click="$emit('click', $event)" @click="$emit('click', $event)"
@edit="$emit('edit', $event)"
@delete="$emit('delete', $event)"
/> />
</view> </view>
</view> </view>
`, `,
}) });;
// ============================================================================ // ============================================================================
// 响应式数据 // 响应式数据
// ============================================================================ // ============================================================================
const searchText = ref("") const searchText = ref("");;
const isRefreshing = ref(false);;
// 部门树形数据(示例数据) // 部门树形数据
const deptList = ref<DeptNode[]>([ const deptList = ref<DeptNode[]>([
{ {
id: "1", id: "1",
@@ -186,47 +261,100 @@ const deptList = ref<DeptNode[]>([
}, },
], ],
}, },
]) ]);;
// 过滤后的部门列表 // 统计数据
const filteredDeptList = computed(() => { const totalDepts = computed(() => {
if (!searchText.value) return deptList.value 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;
function filterDepts(depts: DeptNode[]): DeptNode[] { function filterDepts(depts: DeptNode[]): DeptNode[] {
return depts.reduce<DeptNode[]>((acc, dept) => { return depts.reduce<DeptNode[]>((acc, dept) => {
const matched = dept.label.toLowerCase().includes(searchText.value.toLowerCase()) const matched = dept.label.toLowerCase().includes(searchText.value.toLowerCase());
const filteredChildren = dept.children ? filterDepts(dept.children) : [] cons;t filteredChildren = dept.children ? filterDepts(dept.children) : [];
if (matched || filteredChildren.length > 0) { if (matche;d || filteredChildren.length > 0) {
acc.push({ ac;c.push({
...dept, ...dept,
children: filteredChildren.length > 0 ? filteredChildren : dept.children, 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() { function handleSearch() {;
uni.showToast({ title: `搜索: ${searchText.value}`, icon: "none" }) uni.showToast({ title: `搜索: ${searchText.value}`, icon: "none" });
} }
/** 部门点击 */ /** 部门点击 */
function handleDeptClick(dept: DeptNode) { function handleDeptClick(dept: DeptNode) {;
uni.showToast({ title: `点击: ${dept.label}`, icon: "none" }) 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 handleAddDept() { function handleAddDept() {
uni.showToast({ title: "新增部门", icon: "none" }) uni.showToast({ title: "新增部门", icon: "none" });
} }
</script> </script>
@@ -242,13 +370,70 @@ function handleAddDept() {
// 页面容器 // 页面容器
// ============================================================================ // ============================================================================
.dept-page { .page {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 100vh; min-height: 100vh;
background-color: var(--color-bg-secondary); 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);
}
}
// ============================================================================ // ============================================================================
// 搜索栏 // 搜索栏
// ============================================================================ // ============================================================================
@@ -271,14 +456,47 @@ function handleAddDept() {
} }
// ============================================================================ // ============================================================================
// 部门节点(递归组件样式) // 部门节点
// ============================================================================ // ============================================================================
.dept-node { .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 { &__item {
display: flex; display: flex;
align-items: center; align-items: center;
height: 96rpx; height: 112rpx;
padding-right: 24rpx;
background-color: var(--color-bg);
border-bottom: 2rpx solid var(--color-border-light); border-bottom: 2rpx solid var(--color-border-light);
&:active { &:active {
@@ -290,8 +508,8 @@ function handleAddDept() {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 48rpx; width: 56rpx;
height: 48rpx; height: 56rpx;
} }
&__arrow { &__arrow {
@@ -306,27 +524,47 @@ function handleAddDept() {
&__content { &__content {
display: flex; display: flex;
flex: 1; flex: 1;
gap: 20rpx;
align-items: center; align-items: center;
gap: 16rpx; }
&__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;
} }
&__name { &__name {
font-size: 28rpx; font-size: 30rpx;
font-weight: 500;
color: var(--color-text); color: var(--color-text);
} }
&__meta {
display: flex;
align-items: center;
gap: 8rpx;
padding-right: 24rpx;
}
&__count { &__count {
font-size: 24rpx; font-size: 24rpx;
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
&__chevron {
color: var(--color-text-disabled);
}
&__children { &__children {
background-color: var(--color-bg-secondary); background-color: var(--color-bg-secondary);
} }
@@ -341,11 +579,21 @@ function handleAddDept() {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 100rpx 0; padding: 120rpx 0;
&__text { &__icon-wrap {
margin-top: 24rpx; margin-bottom: 24rpx;
font-size: 28rpx; }
&__title {
font-size: 32rpx;
font-weight: 500;
color: var(--color-text);
}
&__desc {
margin-top: 12rpx;
font-size: 26rpx;
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
} }

View File

@@ -61,7 +61,7 @@ function handleNavClick(item: any) {
{ {
"name": "work", "name": "work",
"style": { "style": {
"navigationBarTitleText": "工作台" "navigationBarTitleText": "工作å<EFBFBD>?
}, },
"layout": "tabbar" "layout": "tabbar"
} }

View File

@@ -1,5 +1,5 @@
<template> <template>
<view class="log"> <view class="page">
<!-- 筛选 --> <!-- 筛选 -->
<wd-drop-menu close-on-click-modal class="mb-24rpx"> <wd-drop-menu close-on-click-modal class="mb-24rpx">
<wd-drop-menu-item ref="filterDropMenu" title="筛选" icon="filter" icon-size="18px"> <wd-drop-menu-item ref="filterDropMenu" title="筛选" icon="filter" icon-size="18px">

View File

@@ -1,5 +1,5 @@
<template> <template>
<view> <view class="page">
<!-- 添加搜索栏 --> <!-- 添加搜索栏 -->
<wd-drop-menu close-on-click-modal class="mb-20rpx mr-20rpx ml-20rpx"> <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"> <wd-drop-menu-item ref="dropMenu" title="筛选" icon="filter" icon-size="18px">

View File

@@ -1,5 +1,5 @@
<template> <template>
<view class="role"> <view class="page">
<!-- 筛选 --> <!-- 筛选 -->
<wd-drop-menu> <wd-drop-menu>
<wd-drop-menu-item ref="filterDropMenu" icon="filter" icon-size="18px" title="筛选"> <wd-drop-menu-item ref="filterDropMenu" icon="filter" icon-size="18px" title="筛选">

View File

@@ -1,5 +1,5 @@
<template> <template>
<view class="user-container"> <view class="page">
<!-- 排序筛选 --> <!-- 排序筛选 -->
<view class="filter-container"> <view class="filter-container">
<wd-drop-menu> <wd-drop-menu>
@@ -71,33 +71,13 @@
</view> </view>
<view class="text-right"> <view class="text-right">
<Auth permission="sys:user:update"> <Auth permission="sys:user:update">
<wd-button <wd-button 编辑 wd-button> bsp; th>uth permission="sys:user:delete">
type="primary" <wd-button type="error" size="small" plain @click="handleDelete(item.id)">
size="small"
plain
@click="handleOpenDialog(item.id)"
>
编辑
</wd-button>
&nbsp;
</Auth>
<Auth permission="sys:user:delete">
<wd-button
type="error"
size="small"
plain
@click="handleDelete(item.id)"
>
删除 删除
</wd-button> </wd-button>
</Auth> </Auth>
</view> </view>
</view> </view>e>e v-if="total > 0" :state="load-tip v-else-if="total == 0" image="search" tip="当前搜索无结果" />
</template>
</wd-card>
<wd-loadmore v-if="total > 0" :state="loadMoreState" @reload="loadmore" />
<wd-status-tip v-else-if="total == 0" image="search" tip="当前搜索无结果" />
</view> </view>
<!-- 弹窗表单 --> <!-- 弹窗表单 -->

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="#3a8ee6">
<rect x="4" y="24" width="8" height="20" rx="2"/>
<rect x="16" y="14" width="8" height="30" rx="2"/>
<rect x="28" y="20" width="8" height="24" rx="2"/>
<rect x="40" y="8" width="8" height="36" rx="2"/>
</svg>

After

Width:  |  Height:  |  Size: 293 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="#e8a838">
<path d="M24 4L8 10v12c0 11 8 18 16 22 8-4 16-11 16-22V10L24 4z"/>
<path d="M20 24l4 4 8-8" stroke="white" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 272 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="#2ab789">
<circle cx="24" cy="16" r="8"/>
<circle cx="10" cy="20" r="6"/>
<circle cx="38" cy="20" r="6"/>
<path d="M24 28c-8 0-14 4-14 8v6h28v-6c0-4-6-8-14-8z"/>
<path d="M10 28c-4 0-8 2-8 5v5h8v-5c0-2 1-4 3-5-1 0-2 0-3 0z"/>
<path d="M38 28c4 0 8 2 8 5v5h-8v-5c0-2-1-4-3-5 1 0 2 0 3 0z"/>
</svg>

After

Width:  |  Height:  |  Size: 375 B

View File

@@ -1,169 +0,0 @@
// ==========================================================================
// BEM Mixins
// ==========================================================================
// 提供便捷的 BEM 命名方式,简化 CSS 类的编写
/// BEM Block 基础 mixin
/// @param {String} $name - Block 名称
/// @example
/// @include b(button) { ... }
/// => .button { ... }
@mixin b($name) {
.#{$name} {
@content;
}
}
/// BEM Element mixin
/// @param {String} $name - Element 名称
/// @example
/// @include e(icon) { ... }
/// => .block__icon { ... }
@mixin e($name) {
&__#{$name} {
@content;
}
}
/// BEM Modifier mixin
/// @param {String} $name - Modifier 名称
/// @example
/// @include m(primary) { ... }
/// => .block--primary { ... }
@mixin m($name) {
&--#{$name} {
@content;
}
}
/// 组合使用示例
/// @example
/// @include b(card) {
/// padding: 16rpx;
///
/// @include e(header) {
/// font-weight: bold;
/// }
///
/// @include m(highlight) {
/// background: yellow;
/// }
/// }
/// => .card { padding: 16rpx; }
/// .card__header { font-weight: bold; }
/// .card--highlight { background: yellow; }
// ==========================================================================
// 布局 Mixins
// ==========================================================================
/// Flex 居中布局
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
/// Flex 两端对齐
@mixin flex-between {
display: flex;
align-items: center;
justify-content: space-between;
}
/// Flex 垂直居中
@mixin flex-col-center {
display: flex;
flex-direction: column;
align-items: center;
}
/// 绝对定位撑满
@mixin absolute-fill {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
/// 固定定位撑满
@mixin fixed-fill {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
// ==========================================================================
// 文字 Mixins
// ==========================================================================
/// 文字省略(单行)
@mixin ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/// 文字省略(多行)
/// @param {Number} $lines - 行数
@mixin ellipsis-lines($lines: 2) {
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: $lines;
-webkit-box-orient: vertical;
}
// ==========================================================================
// 安全区域 Mixins
// ==========================================================================
/// 底部安全区域
@mixin safe-area-bottom {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
/// 顶部安全区域
@mixin safe-area-top {
padding-top: constant(safe-area-inset-top);
padding-top: env(safe-area-inset-top);
}
/// 固定底部(带安全区域)
@mixin fixed-bottom {
position: fixed;
right: 0;
bottom: 0;
left: 0;
@include safe-area-bottom;
}
// ==========================================================================
// 暗黑模式 Mixins
// ==========================================================================
/// 暗黑模式样式
/// @example
/// @include when-dark {
/// background: #1f2937;
/// }
@mixin when-dark {
:global(.wot-theme-dark) & {
@content;
}
}
/// 浅色模式样式
/// @example
/// @include when-light {
/// background: #ffffff;
/// }
@mixin when-light {
:global(:not(.wot-theme-dark)) & {
@content;
}
}

View File

@@ -1,46 +0,0 @@
// ==========================================================================
// SCSS 变量定义
// ==========================================================================
// 用于 SCSS 编译时的变量,与 CSS 变量配合使用
// 设计稿基准
$design-width: 750;
// 字体大小
$font-size-xs: 20rpx;
$font-size-sm: 24rpx;
$font-size-base: 28rpx;
$font-size-md: 32rpx;
$font-size-lg: 36rpx;
$font-size-xl: 40rpx;
$font-size-2xl: 48rpx;
// 间距(4 的倍数)
$spacing-xs: 8rpx;
$spacing-sm: 16rpx;
$spacing-base: 24rpx;
$spacing-md: 32rpx;
$spacing-lg: 48rpx;
$spacing-xl: 64rpx;
// 圆角
$radius-sm: 8rpx;
$radius-base: 12rpx;
$radius-md: 16rpx;
$radius-lg: 24rpx;
$radius-xl: 32rpx;
$radius-full: 9999rpx;
// 过渡时间
$transition-fast: 150ms;
$transition-base: 200ms;
$transition-slow: 300ms;
// 层级
$z-index-dropdown: 100;
$z-index-sticky: 200;
$z-index-fixed: 300;
$z-index-modal-backdrop: 400;
$z-index-modal: 500;
$z-index-popover: 600;
$z-index-toast: 700;

View File

@@ -1,5 +1,3 @@
@use "./variables" as *;
@use "./mixins" as *;
@use "./theme"; @use "./theme";
// ========================================================================== // ==========================================================================
@@ -12,7 +10,8 @@ body,
height: 100%; height: 100%;
padding: 0; padding: 0;
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 28rpx; font-size: 28rpx;
line-height: 1.5; line-height: 1.5;
color: var(--color-text); color: var(--color-text);
@@ -22,93 +21,130 @@ body,
} }
// ========================================================================== // ==========================================================================
// 页面容器 - 分类方案 // 页面容器
// ========================================================================== // ==========================================================================
// 基础容器(无强制高度,适用于大多数页面) .page {
.page-container { background-color: var(--color-bg-tertiary, #f1f5f9);
background-color: var(--color-bg);
} }
// 全屏容器(需要撑满视口的页面,如登录页、启动页) .page--full {
.page-container--full {
min-height: 100vh; min-height: 100vh;
} }
// 自适应容器(内容少的页面,如详情页、表单页) .page--auto {
.page-container--auto {
min-height: auto; min-height: auto;
} }
// 渐变背景页面(用于个人中心等页面) .page--padding {
.page-container--gradient { padding: 0 24rpx;
background: linear-gradient(180deg, #f4f8ff 0, #f8fafc 320rpx, var(--wot-color-bg, #f8fafc) 100%);
} }
// ========================================================================== // ==========================================================================
// 快捷类 // 筛选区域
// ========================================================================== // ==========================================================================
// Flex 布局 .filter {
.flex-center { padding: 16rpx 0;
display: flex;
align-items: center;
justify-content: center;
}
.flex-between { &__search {
display: flex; overflow: hidden;
align-items: center; background: #fff;
justify-content: space-between; border: 1px solid #e2e8f0;
} border-radius: 12rpx;
box-shadow: 0 2rpx 12rpx rgba(15, 23, 42, 0.04);
.flex-col-center { :deep(.wd-search) {
display: flex; background: transparent;
flex-direction: column; }
align-items: center; }
}
// 文字省略 &__bar {
.ellipsis { display: flex;
@include ellipsis; align-items: center;
} 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);
}
.ellipsis-2 { &__item {
@include ellipsis-lines(2); display: flex;
} flex: 1;
align-items: center;
// 安全区域 justify-content: center;
.safe-area-bottom { cursor: pointer;
@include safe-area-bottom;
}
.safe-area-top {
@include safe-area-top;
}
// ==========================================================================
// 通用组件样式
// ==========================================================================
// 空状态占位
.empty-placeholder {
@include flex-center;
flex-direction: column;
padding: 100rpx 32rpx;
color: var(--color-text-secondary);
&__icon {
font-size: 80rpx;
margin-bottom: 24rpx;
} }
&__text { &__text {
margin-right: 8rpx;
font-size: 28rpx; font-size: 28rpx;
color: #64748b;
}
&__divider {
width: 1px;
height: 40rpx;
background: #e2e8f0;
} }
} }
// 加载状态 // ==========================================================================
.loading-container { // 列表区域
@include flex-center; // ==========================================================================
min-height: 200rpx;
.list {
margin-top: 16rpx;
&__card {
margin-bottom: 16rpx;
overflow: hidden;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 12rpx;
box-shadow: 0 2rpx 12rpx rgba(15, 23, 42, 0.04);
&:last-child {
margin-bottom: 0;
}
:deep(.wd-card) {
margin: 0 !important;
background: transparent;
border: none;
box-shadow: none;
}
:deep(.wd-card__body) {
padding: 0 !important;
}
}
}
// ==========================================================================
// 筛选面板
// ==========================================================================
.filter-panel {
padding: 24rpx;
&__footer {
display: flex;
gap: 24rpx;
margin-top: 24rpx;
:deep(.wd-button) {
flex: 1;
}
}
}
// ==========================================================================
// 弹窗底部
// ==========================================================================
.popup-footer {
padding: 30rpx;
} }