978 lines
22 KiB
Vue
978 lines
22 KiB
Vue
<template>
|
||
<view class="page">
|
||
<!-- ============ Hero 头部 ============ -->
|
||
<view class="hero">
|
||
<view class="hero-glow hero-glow-1" />
|
||
<view class="hero-glow hero-glow-2" />
|
||
<view class="hero-top">
|
||
<view class="hero-hello">
|
||
<text class="hero-greet">{{ greeting }}</text>
|
||
<text class="hero-org">{{ orgLine }}</text>
|
||
</view>
|
||
<view class="hero-bell" @tap="goAllApps">
|
||
<FaIcon name="bell" color="#ffffff" :size="20" />
|
||
<text v-if="stats.message > 0" class="bell-badge">{{ badgeText }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="search" @tap="goAllApps">
|
||
<FaIcon name="magnifying-glass" color="#8a94ad" :size="15" />
|
||
<text class="search-text">搜索应用、功能、菜单…</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- ============ 内容区 ============ -->
|
||
<view class="body">
|
||
<!-- KPI 数据条(悬浮压在 Hero 上) -->
|
||
<view class="kpi">
|
||
<view
|
||
v-for="item in kpiList"
|
||
:key="item.label"
|
||
class="kpi-item"
|
||
@tap="item.tap()"
|
||
>
|
||
<text class="kpi-value" :class="{ warn: item.warn }">{{ item.value }}</text>
|
||
<text class="kpi-label">{{ item.label }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 数据加载失败提示(避免把接口故障误显示成「尚未配置」) -->
|
||
<view v-if="loadError" class="err-box" @tap="loadAll">
|
||
<FaIcon name="triangle-exclamation" color="#f04438" :size="18" />
|
||
<view class="err-body">
|
||
<text class="err-title">工作台数据加载失败</text>
|
||
<text class="err-msg">{{ loadError }},点击重试</text>
|
||
</view>
|
||
<FaIcon name="rotate" color="#f04438" :size="16" />
|
||
</view>
|
||
|
||
<!-- 应用宫格:选卡片进入对应功能系统 -->
|
||
<view class="block">
|
||
<view class="block-head">
|
||
<text class="block-title">应用</text>
|
||
<view class="block-more" @tap="goAllApps">
|
||
<text class="block-more-text">全部功能</text>
|
||
<FaIcon name="chevron-right" color="#8a94ad" :size="11" />
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="!loading && !loadError && moduleGroups.length === 0" class="empty-box">
|
||
<text class="empty-text">尚未配置应用模块</text>
|
||
</view>
|
||
|
||
<view v-else-if="!loadError" class="app-grid">
|
||
<view
|
||
v-for="group in moduleGroups"
|
||
:key="group.code || group.id"
|
||
class="app-card"
|
||
:class="{ locked: group.locked }"
|
||
@tap="onModuleTap(group)"
|
||
>
|
||
<view class="app-card-top">
|
||
<view class="tint-icon app-icon" :class="group.tint">
|
||
<FaIcon :name="group.icon" color="#ffffff" :size="24" />
|
||
</view>
|
||
<text v-if="group.locked" class="app-lock">未开通</text>
|
||
</view>
|
||
<text class="app-name">{{ group.name }}</text>
|
||
<text class="app-desc">{{ group.desc }}</text>
|
||
<view class="app-foot">
|
||
<text class="app-count">{{ group.total }} 个功能</text>
|
||
<FaIcon name="arrow-right" color="#c8cfdf" :size="12" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 常用应用 -->
|
||
<view class="block">
|
||
<view class="block-head">
|
||
<text class="block-title">常用功能</text>
|
||
<view class="block-more" @tap="goAllApps">
|
||
<text class="block-more-text">全部应用</text>
|
||
<FaIcon name="chevron-right" color="#8a94ad" :size="11" />
|
||
</view>
|
||
</view>
|
||
<view v-if="!loading && !loadError && quickApps.length === 0" class="empty-box">
|
||
<text class="empty-text">暂无可用应用,请联系管理员开通</text>
|
||
</view>
|
||
<view v-else-if="!loadError" class="grid">
|
||
<view
|
||
v-for="item in quickApps"
|
||
:key="item.key"
|
||
class="grid-item"
|
||
@tap="openMenu(item.menu, item.name)"
|
||
>
|
||
<view class="tint-icon grid-icon" :class="item.tint">
|
||
<FaIcon :name="item.icon" color="#ffffff" :size="22" />
|
||
</view>
|
||
<text class="grid-name">{{ item.name }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 今日待办 -->
|
||
<view class="block">
|
||
<view class="block-head">
|
||
<text class="block-title">今日待办</text>
|
||
<view class="block-more" @tap="goSchedules">
|
||
<text class="block-more-text">全部日程</text>
|
||
<FaIcon name="chevron-right" color="#8a94ad" :size="11" />
|
||
</view>
|
||
</view>
|
||
<view class="list-card">
|
||
<view v-if="pendingList.length === 0" class="empty-box">
|
||
<FaIcon name="circle-check" color="#c0c4cc" :size="26" />
|
||
<text class="empty-text">暂无待办,享受清爽的一天</text>
|
||
</view>
|
||
<view
|
||
v-for="item in pendingList"
|
||
:key="item.id"
|
||
class="list-item"
|
||
@tap="goSchedules"
|
||
>
|
||
<view class="dot" :class="{ warn: isOverdue(item) }" />
|
||
<view class="list-body">
|
||
<text class="list-title">{{ item.content || item.title || '日程' }}</text>
|
||
<text class="list-time" :class="{ warn: isOverdue(item) }">{{ item.schedule_time }}</text>
|
||
</view>
|
||
<FaIcon name="chevron-right" color="#c8cfdf" :size="12" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 最近动态 -->
|
||
<view v-if="activities.length" class="block">
|
||
<view class="block-head">
|
||
<text class="block-title">最近动态</text>
|
||
</view>
|
||
<view class="list-card">
|
||
<view v-for="(item, index) in activities" :key="index" class="list-item">
|
||
<view class="dot" :class="item.type" />
|
||
<view class="list-body">
|
||
<text class="list-title">{{ item.title }}</text>
|
||
<text class="list-time">{{ item.time }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="bottom-space" />
|
||
</view>
|
||
|
||
<AppTabbar current="dashboard" />
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import { onShow } from '@dcloudio/uni-app'
|
||
import { isLoggedIn } from '@/utils/auth.js'
|
||
import { fetchHome } from '@/api/workspace.js'
|
||
import { openMenu, resolveNativeRoute } from '@/utils/nav.js'
|
||
import { request } from '@/api/request.js'
|
||
import AppTabbar from '@/components/MainTabbar.vue'
|
||
|
||
/** 模块 code → 端上图标(后端 icon 存的是 Element Plus 图标名,端上用不了) */
|
||
const MODULE_ICONS = {
|
||
crm: 'handshake',
|
||
erp: 'boxes-stacked',
|
||
oa: 'clipboard-check',
|
||
cms: 'globe',
|
||
ai: 'robot',
|
||
system: 'gear',
|
||
tool: 'wrench'
|
||
}
|
||
|
||
/** 公共菜单(无 module 归属)的协作工具图标 */
|
||
const TOOL_ICONS = ['note-sticky', 'calendar-days', 'file-lines', 'clock-rotate-left', 'chart-line']
|
||
|
||
const loading = ref(true)
|
||
const loadError = ref('')
|
||
const user = ref({})
|
||
const modules = ref([])
|
||
const menus = ref([])
|
||
const stats = ref({ todo: 0, overdue: 0, note: 0, message: 0, reimburse: 0 })
|
||
const pendingList = ref([])
|
||
const activities = ref([])
|
||
|
||
/* ==================== 派生数据 ==================== */
|
||
|
||
const greeting = computed(() => {
|
||
const h = new Date().getHours()
|
||
const slot = h < 6 ? '夜深了' : h < 11 ? '早上好' : h < 13 ? '中午好' : h < 18 ? '下午好' : '晚上好'
|
||
const name = user.value.name
|
||
return name ? slot + ',' + name : slot
|
||
})
|
||
|
||
/** 统一账号:企业 · 账号 · 角色(账号在全平台各系统通用) */
|
||
const orgLine = computed(() => {
|
||
const parts = []
|
||
if (user.value.tenant) parts.push(user.value.tenant)
|
||
if (user.value.account) parts.push(user.value.account)
|
||
if (user.value.role_name) parts.push(user.value.role_name)
|
||
return parts.length ? parts.join(' · ') : '云泽统一账号'
|
||
})
|
||
|
||
const badgeText = computed(() => (stats.value.message > 99 ? '99+' : String(stats.value.message || '')))
|
||
|
||
/** 叶子菜单(可直接跳转的功能页) */
|
||
const leafMenus = computed(() => menus.value.filter((m) => m.leaf !== false && !m.locked))
|
||
|
||
const moduleGroups = computed(() => {
|
||
const byCode = {}
|
||
leafMenus.value.forEach((m) => {
|
||
if (!m.module) return
|
||
if (!byCode[m.module]) byCode[m.module] = []
|
||
byCode[m.module].push(m)
|
||
})
|
||
|
||
// 已登记的功能模块(type=1)。type=2 是系统设置(角色/日志/字典/域名…),
|
||
// 属于后台管理范畴,不适合摆在工作台上,留给「全部应用」页。
|
||
const groups = modules.value
|
||
.filter((m) => m.type !== 2)
|
||
.map((mod, index) => {
|
||
const items = byCode[mod.code] || []
|
||
const code = String(mod.code || '').toLowerCase()
|
||
const locked = mod.enabled === false
|
||
return {
|
||
id: mod.id,
|
||
code: mod.code,
|
||
path: mod.path || '',
|
||
name: mod.name || '未命名应用',
|
||
desc: locked
|
||
? '未开通该模块'
|
||
: mod.description || (items.length ? items.length + ' 个功能' : '暂无功能'),
|
||
icon: MODULE_ICONS[code] || 'cube',
|
||
tint: locked ? 'tint-off' : 'tint-' + (index % 8),
|
||
locked,
|
||
items: items.slice(0, 6),
|
||
total: items.length
|
||
}
|
||
})
|
||
|
||
// 兜底:模块表里没有对应记录(或已删除)的菜单,避免被静默丢弃
|
||
const known = {}
|
||
groups.forEach((g) => {
|
||
known[String(g.code || '').toLowerCase()] = true
|
||
})
|
||
const orphan = []
|
||
leafMenus.value.forEach((m) => {
|
||
const code = String(m.module || '').toLowerCase()
|
||
if (code && !known[code] && code !== 'other') orphan.push(m)
|
||
})
|
||
if (orphan.length) {
|
||
groups.push({
|
||
id: 'other',
|
||
code: 'other',
|
||
name: '其他功能',
|
||
desc: orphan.length + ' 个功能',
|
||
icon: 'file-lines',
|
||
tint: 'tint-7',
|
||
locked: false,
|
||
items: orphan.slice(0, 6),
|
||
total: orphan.length
|
||
})
|
||
}
|
||
|
||
return groups
|
||
})
|
||
|
||
const enabledCount = computed(() => moduleGroups.value.filter((g) => !g.locked).length)
|
||
|
||
/**
|
||
* 协作工具:真正无模块归属的公共菜单(如后续新增的通用功能)。
|
||
* 系统设置类与已归入模块的菜单都不在这里,避免把日志/角色/字典等后台配置
|
||
* 和业务功能混在一块。
|
||
*/
|
||
const commonMenus = computed(() => menus.value.filter((m) => !m.module && m.leaf !== false).slice(0, 6))
|
||
|
||
/** 常用应用:端上原生页面优先,再补各模块首个功能 */
|
||
const quickApps = computed(() => {
|
||
const picked = []
|
||
const seen = new Set()
|
||
|
||
const push = (menu, icon) => {
|
||
if (!menu || seen.has(menu.id) || picked.length >= 8) return
|
||
seen.add(menu.id)
|
||
picked.push({
|
||
key: 'm' + menu.id,
|
||
name: menu.title,
|
||
menu,
|
||
icon: icon || 'circle',
|
||
tint: 'tint-' + (picked.length % 8)
|
||
})
|
||
}
|
||
|
||
leafMenus.value.filter((m) => resolveNativeRoute(m)).forEach((m) => push(m))
|
||
moduleGroups.value.filter((g) => !g.locked).forEach((g) => push(g.items[0], g.icon))
|
||
commonMenus.value.forEach((m, i) => push(m, TOOL_ICONS[i % TOOL_ICONS.length]))
|
||
|
||
return picked
|
||
})
|
||
|
||
const kpiList = computed(() => [
|
||
{ label: '待办日程', value: stats.value.todo || 0, warn: false, tap: goSchedules },
|
||
{ label: '已逾期', value: stats.value.overdue || 0, warn: stats.value.overdue > 0, tap: goSchedules },
|
||
{ label: '报销在途', value: stats.value.reimburse || 0, warn: false, tap: goAllApps },
|
||
{ label: '我的笔记', value: stats.value.note || 0, warn: false, tap: () => openMenu({ title: '记事本' }) }
|
||
])
|
||
|
||
/* ==================== 数据加载 ==================== */
|
||
|
||
async function loadHome() {
|
||
try {
|
||
const data = await fetchHome()
|
||
user.value = data.user || {}
|
||
modules.value = Array.isArray(data.modules) ? data.modules : []
|
||
menus.value = Array.isArray(data.menus) ? data.menus : []
|
||
stats.value = Object.assign({ todo: 0, overdue: 0, note: 0, message: 0, reimburse: 0 }, data.stats || {})
|
||
loadError.value = ''
|
||
} catch (e) {
|
||
// 接口不可用(后端未重启 / 网络异常)时明确暴露原因,
|
||
// 否则会误显示成「尚未配置应用模块」,很难排查
|
||
console.error('[workbench] load home failed:', e)
|
||
loadError.value = (e && e.message) || '接口请求失败'
|
||
}
|
||
}
|
||
|
||
async function loadPending() {
|
||
try {
|
||
const data = await request({ url: '/app/schedule/list', data: { status: 'pending', pageSize: 5 } })
|
||
pendingList.value = (data && data.list ? data.list : []).slice(0, 5)
|
||
} catch (e) {
|
||
pendingList.value = []
|
||
}
|
||
}
|
||
|
||
async function loadActivities() {
|
||
try {
|
||
const data = await request({ url: '/app/activity/list', data: { limit: 5 } })
|
||
const list = Array.isArray(data) ? data : (data && data.list) || []
|
||
activities.value = list.slice(0, 5).map((item) => ({
|
||
type: activityTint(item.target_type),
|
||
title: item.title || (String(item.module || '') + ' ' + String(item.action || '')).trim() || '操作记录',
|
||
time: formatTime(item.created_at)
|
||
}))
|
||
} catch (e) {
|
||
activities.value = []
|
||
}
|
||
}
|
||
|
||
async function loadAll() {
|
||
loading.value = true
|
||
await Promise.all([loadHome(), loadPending(), loadActivities()])
|
||
loading.value = false
|
||
}
|
||
|
||
/* ==================== 交互 ==================== */
|
||
|
||
/**
|
||
* 点击应用卡片 → 进入该功能系统。
|
||
* 优先进入模块下的第一个功能页(各模块的仪表盘/首页,即系统入口),
|
||
* 没有功能页时回落到模块自身 path。
|
||
*/
|
||
function onModuleTap(group) {
|
||
if (group.locked) {
|
||
openMenu({ locked: true, title: group.name })
|
||
return
|
||
}
|
||
if (group.items.length) {
|
||
openMenu(group.items[0], group.name)
|
||
return
|
||
}
|
||
if (group.path) {
|
||
openMenu({ title: group.name, path: group.path, leaf: true }, group.name)
|
||
return
|
||
}
|
||
goAllApps()
|
||
}
|
||
|
||
function goAllApps() {
|
||
uni.navigateTo({ url: '/pages/features/features' })
|
||
}
|
||
|
||
function goSchedules() {
|
||
uni.navigateTo({ url: '/pages/tools/schedule/index' })
|
||
}
|
||
|
||
/* ==================== 工具 ==================== */
|
||
|
||
function isOverdue(item) {
|
||
const t = String(item.schedule_time || item.created_at || '')
|
||
return t.length >= 10 && t.slice(0, 10) < todayStr()
|
||
}
|
||
|
||
function todayStr() {
|
||
const d = new Date()
|
||
const p = (n) => String(n).padStart(2, '0')
|
||
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate())
|
||
}
|
||
|
||
function activityTint(type) {
|
||
const map = {
|
||
note: 'ok',
|
||
notebook: 'ok',
|
||
schedule: 'primary',
|
||
erp: 'warn',
|
||
crm: 'purple',
|
||
file: 'danger'
|
||
}
|
||
return map[type] || 'muted'
|
||
}
|
||
|
||
function formatTime(dateStr) {
|
||
if (!dateStr) return ''
|
||
const ts = new Date(String(dateStr).replace(' ', 'T')).getTime()
|
||
if (!ts) return ''
|
||
const diff = Date.now() - ts
|
||
const min = 60 * 1000
|
||
const hour = 60 * min
|
||
const day = 24 * hour
|
||
if (diff < min) return '刚刚'
|
||
if (diff < hour) return Math.floor(diff / min) + ' 分钟前'
|
||
if (diff < day) return Math.floor(diff / hour) + ' 小时前'
|
||
if (diff < 7 * day) return Math.floor(diff / day) + ' 天前'
|
||
const d = new Date(ts)
|
||
return d.getMonth() + 1 + '/' + d.getDate()
|
||
}
|
||
|
||
/* ==================== 生命周期 ==================== */
|
||
|
||
onMounted(() => {
|
||
if (!isLoggedIn()) {
|
||
uni.reLaunch({ url: '/pages/login/login' })
|
||
return
|
||
}
|
||
loadAll()
|
||
})
|
||
|
||
onShow(() => {
|
||
if (isLoggedIn() && !loading.value) loadAll()
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
@import '@/src/styles/page-common.scss';
|
||
|
||
.page {
|
||
@include page-shell;
|
||
}
|
||
|
||
/* ============ Hero ============ */
|
||
.hero {
|
||
position: relative;
|
||
/* 与 .kpi 显式分层:不依赖渲染器对 position:relative 同级元素的隐式绘制顺序,
|
||
否则部分 WebView 下 .kpi 的负边距上移部分会被本层裁掉 */
|
||
z-index: 1;
|
||
flex-shrink: 0;
|
||
padding: calc(var(--status-bar-height, 44px) + 28rpx) $page-padding-x 76rpx;
|
||
background: $hero-gradient;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.hero-glow {
|
||
position: absolute;
|
||
border-radius: 50%;
|
||
filter: blur(50rpx);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.hero-glow-1 {
|
||
width: 380rpx;
|
||
height: 380rpx;
|
||
top: -160rpx;
|
||
right: -120rpx;
|
||
background: rgba(255, 255, 255, 0.18);
|
||
}
|
||
|
||
.hero-glow-2 {
|
||
width: 300rpx;
|
||
height: 300rpx;
|
||
bottom: -140rpx;
|
||
left: -100rpx;
|
||
background: rgba(255, 255, 255, 0.12);
|
||
}
|
||
|
||
.hero-top {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.hero-hello {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.hero-greet {
|
||
display: block;
|
||
font-size: 44rpx;
|
||
font-weight: 600;
|
||
color: #ffffff;
|
||
letter-spacing: 1rpx;
|
||
}
|
||
|
||
.hero-org {
|
||
display: block;
|
||
margin-top: 10rpx;
|
||
font-size: 24rpx;
|
||
color: rgba(255, 255, 255, 0.78);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.hero-bell {
|
||
position: relative;
|
||
width: 72rpx;
|
||
height: 72rpx;
|
||
border-radius: 50%;
|
||
background: rgba(255, 255, 255, 0.16);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.bell-badge {
|
||
position: absolute;
|
||
top: 6rpx;
|
||
right: 4rpx;
|
||
min-width: 30rpx;
|
||
height: 30rpx;
|
||
padding: 0 6rpx;
|
||
border-radius: 15rpx;
|
||
background: #f04438;
|
||
color: #ffffff;
|
||
font-size: 18rpx;
|
||
line-height: 30rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.search {
|
||
position: relative;
|
||
margin-top: 32rpx;
|
||
height: 76rpx;
|
||
padding: 0 24rpx;
|
||
border-radius: $radius-full;
|
||
background: #ffffff;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14rpx;
|
||
box-shadow: 0 8rpx 20rpx rgba(17, 35, 79, 0.12);
|
||
}
|
||
|
||
.search-text {
|
||
font-size: 26rpx;
|
||
color: $color-text-muted;
|
||
}
|
||
|
||
/* ============ 内容区 ============ */
|
||
.body {
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow-y: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
padding: 0 $page-padding-x;
|
||
}
|
||
|
||
/* KPI 悬浮卡 */
|
||
/* KPI 悬浮卡:上移量 = 顶部内边距,保证数字恰好落在 Hero 下边缘之后 */
|
||
.kpi {
|
||
position: relative;
|
||
z-index: 2;
|
||
margin-top: -44rpx;
|
||
display: flex;
|
||
@include card;
|
||
padding: 44rpx 0 28rpx;
|
||
box-shadow: $shadow-float;
|
||
}
|
||
|
||
.kpi-item {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 6rpx;
|
||
position: relative;
|
||
|
||
& + &::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0;
|
||
top: 8rpx;
|
||
bottom: 8rpx;
|
||
width: 1rpx;
|
||
background: $color-divider;
|
||
}
|
||
}
|
||
|
||
.kpi-value {
|
||
font-size: 40rpx;
|
||
font-weight: 600;
|
||
color: $color-text;
|
||
line-height: 1.2;
|
||
|
||
&.warn {
|
||
color: $color-danger;
|
||
}
|
||
}
|
||
|
||
.kpi-label {
|
||
font-size: 22rpx;
|
||
color: $color-text-muted;
|
||
}
|
||
|
||
/* ============ 加载失败提示 ============ */
|
||
.err-box {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
margin-top: 24rpx;
|
||
padding: 24rpx 22rpx;
|
||
background: #fef3f2;
|
||
border: 1rpx solid #fdd8d4;
|
||
border-radius: $radius-md;
|
||
}
|
||
|
||
.err-body {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.err-title {
|
||
display: block;
|
||
font-size: 27rpx;
|
||
font-weight: 500;
|
||
color: $color-danger;
|
||
}
|
||
|
||
.err-msg {
|
||
display: block;
|
||
margin-top: 6rpx;
|
||
font-size: 22rpx;
|
||
color: #b42318;
|
||
word-break: break-all;
|
||
}
|
||
|
||
/* ============ 区块通用 ============ */
|
||
.block {
|
||
margin-top: 36rpx;
|
||
}
|
||
.block-head {
|
||
@include block-head;
|
||
}
|
||
|
||
.block-title {
|
||
@include section-title;
|
||
}
|
||
|
||
.block-sub {
|
||
font-size: 24rpx;
|
||
color: $color-text-muted;
|
||
}
|
||
|
||
.block-more {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4rpx;
|
||
}
|
||
|
||
.block-more-text {
|
||
font-size: 24rpx;
|
||
color: $color-text-muted;
|
||
}
|
||
|
||
.empty-box {
|
||
@include card;
|
||
padding: 56rpx 24rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.empty-text {
|
||
font-size: 25rpx;
|
||
color: $color-text-muted;
|
||
}
|
||
|
||
/* ============ 应用宫格卡片 ============ */
|
||
.app-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.app-card {
|
||
@include card;
|
||
padding: 26rpx 24rpx 20rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
transition: transform 0.15s ease, box-shadow 0.2s ease;
|
||
|
||
&:active {
|
||
transform: scale(0.97);
|
||
}
|
||
|
||
&.locked {
|
||
.app-name,
|
||
.app-desc {
|
||
color: $color-text-muted;
|
||
}
|
||
}
|
||
}
|
||
|
||
.app-card-top {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.app-icon {
|
||
@include tint-icon(88rpx, 24rpx);
|
||
}
|
||
|
||
.app-lock {
|
||
@include chip(#fef3f2, $color-danger);
|
||
}
|
||
|
||
.app-name {
|
||
margin-top: 20rpx;
|
||
font-size: 30rpx;
|
||
font-weight: 600;
|
||
color: $color-text;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.app-desc {
|
||
margin-top: 6rpx;
|
||
font-size: 22rpx;
|
||
color: $color-text-muted;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.app-foot {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-top: 18rpx;
|
||
padding-top: 16rpx;
|
||
border-top: 1rpx solid $color-divider;
|
||
}
|
||
|
||
.app-count {
|
||
font-size: 22rpx;
|
||
color: $color-primary;
|
||
}
|
||
|
||
/* ============ 常用功能宫格 ============ */
|
||
.grid {
|
||
@include card;
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
padding: 28rpx 8rpx 20rpx;
|
||
}
|
||
|
||
.grid-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 14rpx;
|
||
padding: 12rpx 0;
|
||
|
||
&:active {
|
||
opacity: 0.7;
|
||
}
|
||
}
|
||
|
||
.grid-icon {
|
||
@include tint-icon(92rpx, 24rpx);
|
||
}
|
||
|
||
.grid-name {
|
||
font-size: 23rpx;
|
||
color: $color-text-secondary;
|
||
text-align: center;
|
||
}
|
||
|
||
/* ============ 应用中心卡片 ============ */
|
||
.mod-card {
|
||
@include card;
|
||
padding: 24rpx;
|
||
margin-bottom: 20rpx;
|
||
|
||
&.locked {
|
||
.mod-name,
|
||
.mod-desc {
|
||
color: $color-text-muted;
|
||
}
|
||
}
|
||
}
|
||
|
||
.mod-head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.mod-icon {
|
||
@include tint-icon(88rpx, 22rpx);
|
||
}
|
||
|
||
.mod-meta {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.mod-title-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.mod-name {
|
||
font-size: 30rpx;
|
||
font-weight: 600;
|
||
color: $color-text;
|
||
}
|
||
|
||
.mod-lock {
|
||
@include chip(#fef3f2, $color-danger);
|
||
}
|
||
|
||
.mod-desc {
|
||
display: block;
|
||
margin-top: 6rpx;
|
||
font-size: 22rpx;
|
||
color: $color-text-muted;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.chips {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 14rpx;
|
||
margin-top: 20rpx;
|
||
padding-top: 20rpx;
|
||
border-top: 1rpx solid $color-divider;
|
||
}
|
||
|
||
.chip {
|
||
@include chip;
|
||
}
|
||
|
||
.chip-off {
|
||
background: $color-divider;
|
||
color: $color-text-muted;
|
||
}
|
||
|
||
.chip-more {
|
||
background: $color-primary-bg;
|
||
}
|
||
|
||
/* ============ 协作工具 ============ */
|
||
.tool-card {
|
||
@include card;
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
padding: 24rpx 8rpx;
|
||
}
|
||
|
||
.tool-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
padding: 10rpx 0;
|
||
|
||
&:active {
|
||
opacity: 0.7;
|
||
}
|
||
}
|
||
|
||
.tool-icon {
|
||
@include icon-box(72rpx);
|
||
}
|
||
|
||
.tool-name {
|
||
font-size: 23rpx;
|
||
color: $color-text-secondary;
|
||
text-align: center;
|
||
}
|
||
|
||
/* ============ 列表(待办 / 动态) ============ */
|
||
.list-card {
|
||
@include card;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.list-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 18rpx;
|
||
padding: 26rpx 24rpx;
|
||
|
||
& + & {
|
||
border-top: 1rpx solid $color-divider;
|
||
}
|
||
|
||
&:active {
|
||
background: $color-bg-page;
|
||
}
|
||
}
|
||
|
||
.dot {
|
||
width: 14rpx;
|
||
height: 14rpx;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
background: $color-text-muted;
|
||
|
||
&.primary { background: $color-primary; }
|
||
&.ok { background: $color-success; }
|
||
&.warn { background: $color-warning; }
|
||
&.danger { background: $color-danger; }
|
||
&.purple { background: #7c3aed; }
|
||
&.muted { background: $color-text-muted; }
|
||
}
|
||
|
||
.list-body {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.list-title {
|
||
display: block;
|
||
font-size: 27rpx;
|
||
color: $color-text;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.list-time {
|
||
display: block;
|
||
margin-top: 6rpx;
|
||
font-size: 22rpx;
|
||
color: $color-text-muted;
|
||
|
||
&.warn {
|
||
color: $color-danger;
|
||
}
|
||
}
|
||
|
||
.bottom-space {
|
||
height: 40rpx;
|
||
}
|
||
</style>
|