first commit
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
<template>
|
||||
<view class="business-data-page">
|
||||
<!-- 使用子头部组件 -->
|
||||
<sub-header
|
||||
title="业务数据总览"
|
||||
@goBack="goBack"
|
||||
@showMoreOptions="showMoreOptions"
|
||||
/>
|
||||
|
||||
<!-- 数据分类列表 -->
|
||||
<view class="data-categories">
|
||||
<view
|
||||
class="category-item"
|
||||
v-for="category in dataCategories"
|
||||
:key="category.id"
|
||||
@click="navigateToDetail(category)"
|
||||
>
|
||||
<view
|
||||
class="category-icon"
|
||||
:style="{ backgroundColor: category.bgColor }"
|
||||
>
|
||||
<i :class="category.icon"></i>
|
||||
</view>
|
||||
<view class="category-content">
|
||||
<view class="category-title">{{ category.title }}</view>
|
||||
<view class="category-desc">{{ category.description }}</view>
|
||||
<view class="category-stats">
|
||||
<text class="stat-item">{{ category.dataCount }}个数据源</text>
|
||||
<text class="stat-item">{{ category.updateTime }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="category-arrow">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import SubHeader from "../components/subHeader.vue";
|
||||
|
||||
// 数据分类列表
|
||||
const dataCategories = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: "港口数据",
|
||||
description: "港口吞吐量等相关数据",
|
||||
icon: "fas fa-chart-line",
|
||||
bgColor: "#4ECDC4",
|
||||
dataCount: 8,
|
||||
updateTime: "2小时前更新",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "财务数据",
|
||||
description: "收入、支出、报销、预算等财务相关数据",
|
||||
icon: "fas fa-money-bill-wave",
|
||||
bgColor: "#FECA57",
|
||||
dataCount: 12,
|
||||
updateTime: "1小时前更新",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "客户数据",
|
||||
description: "客户信息、满意度、活跃度等客户相关数据",
|
||||
icon: "fas fa-user-friends",
|
||||
bgColor: "#45B7D1",
|
||||
dataCount: 6,
|
||||
updateTime: "30分钟前更新",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "运营数据",
|
||||
description: "访问量、转化率、用户行为等运营相关数据",
|
||||
icon: "fas fa-chart-bar",
|
||||
bgColor: "#FF6B6B",
|
||||
dataCount: 10,
|
||||
updateTime: "15分钟前更新",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: "人力资源数据",
|
||||
description: "员工信息、考勤、绩效等人力资源相关数据",
|
||||
icon: "fas fa-users",
|
||||
bgColor: "#96CEB4",
|
||||
dataCount: 15,
|
||||
updateTime: "1天前更新",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: "项目数据",
|
||||
description: "项目进度、任务完成情况、资源分配等项目相关数据",
|
||||
icon: "fas fa-project-diagram",
|
||||
bgColor: "#A8E6CF",
|
||||
dataCount: 9,
|
||||
updateTime: "3小时前更新",
|
||||
},
|
||||
]);
|
||||
|
||||
// 导航到详情页面
|
||||
const navigateToDetail = (category) => {
|
||||
// 特殊处理港口数据,直接跳转到portData页面
|
||||
if (category.id === 1) {
|
||||
uni.navigateTo({
|
||||
url: "/pages/businessDatas/portData",
|
||||
});
|
||||
} else {
|
||||
// 其他数据跳转到通用详情页面
|
||||
uni.navigateTo({
|
||||
url: `/pages/businessDatas/detail?categoryId=${category.id}&title=${category.title}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 头部相关方法
|
||||
const goBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
const showMoreOptions = () => {
|
||||
uni.showActionSheet({
|
||||
itemList: ["刷新", "返回首页"],
|
||||
success: (res) => {
|
||||
switch (res.tapIndex) {
|
||||
case 0:
|
||||
// 刷新数据
|
||||
uni.reLaunch({
|
||||
url: "/pages/businessDatas/index",
|
||||
});
|
||||
uni.showToast({ title: "数据已刷新", icon: "success" });
|
||||
break;
|
||||
case 1:
|
||||
uni.reLaunch({
|
||||
url: "/pages/index/index",
|
||||
});
|
||||
uni.showToast({ title: "已返回工作台", icon: "success" });
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.business-data-page {
|
||||
background: #f9f9fb;
|
||||
min-height: 100vh;
|
||||
padding-top: calc(var(--status-bar-height) + 88rpx);
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
/* 支持安全区域的设备 */
|
||||
@supports (padding: max(0px)) {
|
||||
.business-data-page {
|
||||
padding-top: calc(
|
||||
var(--status-bar-height) + 88rpx + env(safe-area-inset-top)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* 数据分类列表 */
|
||||
.data-categories {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.category-item {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all 0.3s ease;
|
||||
border: 1rpx solid #f0f0f0;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24rpx;
|
||||
flex-shrink: 0;
|
||||
|
||||
i {
|
||||
font-size: 36rpx;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.category-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.category-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.category-desc {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
margin-bottom: 12rpx;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.category-stats {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
background: #f5f5f5;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 12rpx;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.category-arrow {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ccc;
|
||||
flex-shrink: 0;
|
||||
|
||||
i {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,810 @@
|
||||
<template>
|
||||
<view class="data-detail-page" :class="{ mobile: isMobile }">
|
||||
<!-- 页面头部 -->
|
||||
<sub-header
|
||||
title="港口数据"
|
||||
@goBack="goBack"
|
||||
@showMoreOptions="showMoreOptions"
|
||||
/>
|
||||
|
||||
<!-- 数据卡片 -->
|
||||
<view class="cards-section">
|
||||
<view class="section-title">数据概览</view>
|
||||
<view class="card-grid">
|
||||
<view v-for="card in cardData" :key="card.id" class="data-card">
|
||||
<view class="card-icon">
|
||||
<i :class="card.icon"></i>
|
||||
</view>
|
||||
<view class="card-content">
|
||||
<text class="card-value">{{ card.value }}</text>
|
||||
<text class="card-label">{{ card.title }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 图表分析 -->
|
||||
<view class="charts-section">
|
||||
<view class="section-title">图表分析</view>
|
||||
<view class="chart-tabs">
|
||||
<view
|
||||
class="chart-tab"
|
||||
:class="{ active: activeTab === 'cargo' }"
|
||||
@click="switchTab('cargo')"
|
||||
>
|
||||
船舶作业货物信息
|
||||
</view>
|
||||
<view
|
||||
class="chart-tab"
|
||||
:class="{ active: activeTab === 'customer' }"
|
||||
@click="switchTab('customer')"
|
||||
>
|
||||
客户排名
|
||||
</view>
|
||||
<view
|
||||
class="chart-tab"
|
||||
:class="{ active: activeTab === 'location' }"
|
||||
@click="switchTab('location')"
|
||||
>
|
||||
作业地点作业量
|
||||
</view>
|
||||
</view>
|
||||
<view class="chart-content">
|
||||
<!-- 船舶作业货物信息 -->
|
||||
<view v-show="activeTab === 'cargo'" class="charts-container">
|
||||
<view class="chartsMain">
|
||||
<l-echart ref="chartRef" type="2d" :height="'auto'" />
|
||||
</view>
|
||||
<view class="chartsMain">
|
||||
<l-echart ref="cargoChartRef" type="2d" :height="'auto'" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 客户排名 -->
|
||||
<view v-show="activeTab === 'customer'" class="charts-container">
|
||||
<view class="chartsMain">
|
||||
<l-echart ref="customerChartRef" type="2d" :height="'auto'" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 作业地点作业量 -->
|
||||
<view v-show="activeTab === 'location'" class="charts-container">
|
||||
<view class="chartsMain">
|
||||
<l-echart ref="locationChartRef" type="2d" :height="'auto'" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<view class="table-section">
|
||||
<view class="section-title">详细数据</view>
|
||||
<view class="table-container">
|
||||
<view class="data-table">
|
||||
<view class="table-header">
|
||||
<view
|
||||
v-for="header in tableHeaders"
|
||||
:key="header.key"
|
||||
class="table-cell header-cell"
|
||||
>
|
||||
{{ header.title }}
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
v-for="(row, index) in tableData"
|
||||
:key="index"
|
||||
class="table-row"
|
||||
>
|
||||
<view
|
||||
v-for="header in tableHeaders"
|
||||
:key="header.key"
|
||||
class="table-cell"
|
||||
>
|
||||
{{ row[header.key] }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, nextTick } from "vue";
|
||||
import SubHeader from "../components/subHeader.vue";
|
||||
import * as echarts from "echarts";
|
||||
|
||||
// 声明uni全局对象
|
||||
declare const uni: any;
|
||||
|
||||
// 声明全局类型
|
||||
declare global {
|
||||
interface Window {
|
||||
uni: any;
|
||||
}
|
||||
}
|
||||
|
||||
// 类型定义
|
||||
interface CardData {
|
||||
id: number;
|
||||
title: string;
|
||||
value: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
interface TableHeader {
|
||||
key: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface TableData {
|
||||
port: string;
|
||||
berth: string;
|
||||
ship: string;
|
||||
cargo: string;
|
||||
weight: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
// 响应式数据
|
||||
const pageTitle = ref("港口数据");
|
||||
const activeTab = ref("cargo");
|
||||
const chartRef = ref<any>(null);
|
||||
const cargoChartRef = ref<any>(null);
|
||||
const customerChartRef = ref<any>(null);
|
||||
const locationChartRef = ref<any>(null);
|
||||
|
||||
// 设备信息
|
||||
const isMobile = ref(false);
|
||||
|
||||
const cardData = ref<CardData[]>([
|
||||
{ id: 1, title: "报港次数", value: "173次", icon: "fas fa-ship" },
|
||||
{ id: 2, title: "完船次数", value: "197次", icon: "fas fa-anchor" },
|
||||
{
|
||||
id: 3,
|
||||
title: "作业票作业量",
|
||||
value: "260193.687吨",
|
||||
icon: "fas fa-weight",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "船舶报港吨数",
|
||||
value: "218928.400吨",
|
||||
icon: "fas fa-ship",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: "完船作业吨数",
|
||||
value: "252643.653吨",
|
||||
icon: "fas fa-anchor",
|
||||
},
|
||||
]);
|
||||
|
||||
const tableHeaders = ref<TableHeader[]>([
|
||||
{ key: "port", title: "港口" },
|
||||
{ key: "berth", title: "泊位" },
|
||||
{ key: "ship", title: "船舶" },
|
||||
{ key: "cargo", title: "货物" },
|
||||
{ key: "weight", title: "重量(吨)" },
|
||||
{ key: "status", title: "状态" },
|
||||
]);
|
||||
|
||||
const tableData = ref<TableData[]>([
|
||||
{
|
||||
port: "上海港",
|
||||
berth: "A1",
|
||||
ship: "中远海运",
|
||||
cargo: "集装箱",
|
||||
weight: "50000",
|
||||
status: "作业中",
|
||||
},
|
||||
{
|
||||
port: "宁波港",
|
||||
berth: "B2",
|
||||
ship: "马士基",
|
||||
cargo: "散货",
|
||||
weight: "75000",
|
||||
status: "已完成",
|
||||
},
|
||||
{
|
||||
port: "青岛港",
|
||||
berth: "C3",
|
||||
ship: "长荣海运",
|
||||
cargo: "油品",
|
||||
weight: "30000",
|
||||
status: "待作业",
|
||||
},
|
||||
]);
|
||||
|
||||
// 方法
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
const showMoreOptions = () => {
|
||||
uni.showActionSheet({
|
||||
itemList: ["刷新", "返回首页"],
|
||||
success: (res) => {
|
||||
switch (res.tapIndex) {
|
||||
case 0:
|
||||
// 刷新数据
|
||||
uni.reLaunch({
|
||||
url: "/pages/businessDatas/portData",
|
||||
});
|
||||
uni.showToast({ title: "数据已刷新", icon: "success" });
|
||||
break;
|
||||
//返回首页
|
||||
case 1:
|
||||
uni.reLaunch({
|
||||
url: "/pages/index/index",
|
||||
});
|
||||
uni.showToast({ title: "已返回工作台", icon: "success" });
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 客户排名原始数据
|
||||
const customerRankingRawData = [
|
||||
{ name: "江苏鑫顺通新材料有限公司", value: 64641.52 },
|
||||
{ name: "连云港耀捷建设工程有限公司", value: 44569.54 },
|
||||
{ name: "江苏德邦兴华化工科技有限公司", value: 27887.37 },
|
||||
{ name: "连云港海鲲物流有限公司", value: 19028.98 },
|
||||
{ name: "张家港红喜物流有限公司", value: 16538.4 },
|
||||
{ name: "成忠", value: 15557.77 },
|
||||
{ name: "江苏米耘环保科技有限公司", value: 13952.03 },
|
||||
{ name: "南京恒之通船务有限公司", value: 12684 },
|
||||
{ name: "连云港市瑞豪贸易有限公司", value: 8098 },
|
||||
{ name: "佰鼎风禾(江苏)供应链管理有限公司", value: 6546.48 },
|
||||
];
|
||||
|
||||
// 作业地点作业量原始数据
|
||||
const locationWorkloadRawData = [
|
||||
{ name: "抓1", value: 12163.26 },
|
||||
{ name: "抓2", value: 11872.35 },
|
||||
{ name: "倒台", value: 93673.52 },
|
||||
{ name: "1#", value: 59712.06 },
|
||||
{ name: "2#", value: 9359.546 },
|
||||
{ name: "3#", value: 8825.58 },
|
||||
{ name: "4#", value: 14898.896 },
|
||||
{ name: "5#", value: 19450.075 },
|
||||
{ name: "6#", value: 17650.25 },
|
||||
{ name: "7#", value: 21368.56 },
|
||||
{ name: "8#", value: 35275.316 },
|
||||
{ name: "抓2对面", value: 222.0 },
|
||||
{ name: "二期大库", value: 9.1 },
|
||||
{ name: "调度室后", value: 679.966 },
|
||||
{ name: "直灌", value: 0 },
|
||||
];
|
||||
|
||||
// 自动排序客户排名数据
|
||||
const customerRankingData = (() => {
|
||||
// 按数值从小到大排序
|
||||
const sortedData = customerRankingRawData.sort((a, b) => a.value - b.value);
|
||||
|
||||
return {
|
||||
legend: ["10月"],
|
||||
yAxisData: sortedData.map((item) => item.name),
|
||||
seriesData: sortedData.map((item) => item.value),
|
||||
};
|
||||
})();
|
||||
|
||||
// 作业地点作业量数据(不排序,按原始顺序)
|
||||
const locationWorkloadData = {
|
||||
legend: ["10月"],
|
||||
yAxisData: locationWorkloadRawData.map((item) => item.name),
|
||||
seriesData: locationWorkloadRawData.map((item) => item.value),
|
||||
};
|
||||
|
||||
// 初始化客户排名图表
|
||||
const initCustomerChart = (chart: any) => {
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
axisPointer: {
|
||||
type: "shadow",
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
grid: {
|
||||
left: "3%",
|
||||
right: "8%",
|
||||
bottom: "3%",
|
||||
top: "5%",
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: "value",
|
||||
boundaryGap: [0, 0.01],
|
||||
axisLine: {
|
||||
show: false,
|
||||
},
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
axisLabel: {
|
||||
show: false,
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: "category",
|
||||
data: customerRankingData.yAxisData,
|
||||
axisLabel: {
|
||||
fontSize: 12,
|
||||
width: 80,
|
||||
overflow: "truncate",
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: customerRankingData.legend[0],
|
||||
type: "bar",
|
||||
data: customerRankingData.seriesData,
|
||||
label: {
|
||||
show: true,
|
||||
position: "right",
|
||||
formatter: "{c}",
|
||||
fontSize: 11,
|
||||
color: "#333",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
chart.setOption(option);
|
||||
console.log("客户排名图表初始化成功");
|
||||
};
|
||||
|
||||
// 初始化作业地点作业量图表
|
||||
const initLocationChart = (chart: any) => {
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
axisPointer: {
|
||||
type: "shadow",
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
grid: {
|
||||
left: "3%",
|
||||
right: "8%",
|
||||
bottom: "3%",
|
||||
top: "5%",
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: "value",
|
||||
boundaryGap: [0, 0.01],
|
||||
axisLine: {
|
||||
show: false,
|
||||
},
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
axisLabel: {
|
||||
show: false,
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: "category",
|
||||
data: locationWorkloadData.yAxisData,
|
||||
axisLabel: {
|
||||
fontSize: 12,
|
||||
width: 80,
|
||||
overflow: "truncate",
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: locationWorkloadData.legend[0],
|
||||
type: "bar",
|
||||
data: locationWorkloadData.seriesData,
|
||||
label: {
|
||||
show: true,
|
||||
position: "right",
|
||||
formatter: "{c}",
|
||||
fontSize: 11,
|
||||
color: "#333",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
chart.setOption(option);
|
||||
console.log("作业地点作业量图表初始化成功");
|
||||
};
|
||||
|
||||
// 切换tab
|
||||
const switchTab = (tab: string) => {
|
||||
activeTab.value = tab;
|
||||
|
||||
// 如果切换到客户排名tab,确保图表正确显示
|
||||
if (tab === "customer") {
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
if (customerChartRef.value) {
|
||||
customerChartRef.value.init(echarts, initCustomerChart);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
// 如果切换到作业地点作业量tab,确保图表正确显示
|
||||
if (tab === "location") {
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
if (locationChartRef.value) {
|
||||
locationChartRef.value.init(echarts, initLocationChart);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
// 检测设备类型
|
||||
try {
|
||||
const systemInfo = uni.getSystemInfoSync();
|
||||
isMobile.value =
|
||||
systemInfo.platform !== "devtools" &&
|
||||
(systemInfo.platform === "ios" || systemInfo.platform === "android");
|
||||
} catch (e) {
|
||||
isMobile.value = false;
|
||||
}
|
||||
|
||||
// 初始化第一个图表 - 货物类型统计
|
||||
chartRef.value.init(echarts, (chart) => {
|
||||
const option = {
|
||||
title: {
|
||||
text: "货物类型统计",
|
||||
subtext: "各类型作业量分布",
|
||||
left: "center",
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
},
|
||||
legend: {
|
||||
orient: "horizontal",
|
||||
bottom: "bottom",
|
||||
itemGap: 10,
|
||||
itemWidth: 14,
|
||||
itemHeight: 14,
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: "作业类型",
|
||||
type: "pie",
|
||||
radius: "50%",
|
||||
center: ["50%", "50%"],
|
||||
data: [
|
||||
{ value: 67187.753, name: "门机件杂货" },
|
||||
{ value: 26582.55, name: "门机散货" },
|
||||
{ value: 15576.03, name: "抓机散货" },
|
||||
{ value: 82319.55, name: "倒台散货" },
|
||||
{ value: 45963.8, name: "挖机散货" },
|
||||
],
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: "rgba(0, 0, 0, 0.5)",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
chart.setOption(option);
|
||||
});
|
||||
|
||||
// 初始化第二个图表 - 货物品类统计
|
||||
cargoChartRef.value.init(echarts, (chart) => {
|
||||
const option = {
|
||||
title: {
|
||||
text: "货物品类统计",
|
||||
subtext: "各类货物占比分布",
|
||||
left: "center",
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
},
|
||||
legend: {
|
||||
orient: "vertical",
|
||||
right: "right",
|
||||
itemGap: 5,
|
||||
itemWidth: 14,
|
||||
itemHeight: 14,
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: "货物品类",
|
||||
type: "pie",
|
||||
radius: "50%",
|
||||
center: ["50%", "55%"],
|
||||
data: [
|
||||
{ value: 64641.52, name: "铁精粉" },
|
||||
{ value: 44734.8, name: "PTA" },
|
||||
{ value: 44569.54, name: "粉质粘土" },
|
||||
{ value: 33427.37, name: "盐" },
|
||||
{ value: 15557.77, name: "石粉砂" },
|
||||
{ value: 13952.03, name: "低卡煤" },
|
||||
{ value: 4804.04, name: "氯化钾" },
|
||||
{ value: 4140.0, name: "元明粉" },
|
||||
{ value: 3664.0, name: "轻碱" },
|
||||
{ value: 23152.583, name: "其他" },
|
||||
],
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: "rgba(0, 0, 0, 0.5)",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
chart.setOption(option);
|
||||
});
|
||||
|
||||
// 初始化客户排名图表 - 延迟初始化
|
||||
setTimeout(() => {
|
||||
if (customerChartRef.value) {
|
||||
customerChartRef.value.init(echarts, initCustomerChart);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// 初始化作业地点作业量图表 - 延迟初始化
|
||||
setTimeout(() => {
|
||||
if (locationChartRef.value) {
|
||||
locationChartRef.value.init(echarts, initLocationChart);
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.data-detail-page {
|
||||
min-height: 100vh;
|
||||
background: var(--background);
|
||||
padding-top: 88rpx;
|
||||
}
|
||||
|
||||
/* 移动设备需要额外的状态栏高度 */
|
||||
.data-detail-page.mobile {
|
||||
padding-top: calc(var(--status-bar-height) + 88rpx);
|
||||
}
|
||||
|
||||
/* 支持安全区域的设备 */
|
||||
@supports (padding: max(0px)) {
|
||||
.data-detail-page.mobile {
|
||||
padding-top: calc(
|
||||
var(--status-bar-height) + 88rpx + env(safe-area-inset-top)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--title-color);
|
||||
margin-bottom: 24rpx;
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
/* 卡片样式 */
|
||||
.cards-section {
|
||||
padding: 30rpx 0;
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20rpx;
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
.data-card {
|
||||
background: var(--surface);
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
border: 1rpx solid var(--border-light);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
background: var(--gradient-primary);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card-icon i {
|
||||
font-size: 32rpx;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: var(--title-color);
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
/* 图表样式 */
|
||||
.charts-section {
|
||||
padding: 30rpx 0;
|
||||
}
|
||||
|
||||
.chart-tabs {
|
||||
display: flex;
|
||||
padding: 0 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chart-tabs::-webkit-scrollbar {
|
||||
display: none; /* Chrome, Safari and Opera */
|
||||
}
|
||||
|
||||
.chart-tab {
|
||||
flex-shrink: 0;
|
||||
padding: 16rpx 32rpx;
|
||||
margin-right: 16rpx;
|
||||
background: var(--gray-light);
|
||||
border-radius: 32rpx;
|
||||
font-size: 28rpx;
|
||||
color: var(--text-color);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chart-tab:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.chart-tab.active {
|
||||
background: var(--primary-color);
|
||||
color: var(--white);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.chart-content {
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
/* vchart样式 */
|
||||
.chartsMain {
|
||||
width: 100%;
|
||||
min-height: 400rpx;
|
||||
padding: 15rpx;
|
||||
background: var(--surface);
|
||||
margin-bottom: 24rpx;
|
||||
border-radius: 12rpx;
|
||||
box-shadow: var(--shadow-md);
|
||||
border: 1rpx solid var(--border-light);
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 移动端优化 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.chartsMain {
|
||||
min-height: 300rpx;
|
||||
}
|
||||
}
|
||||
|
||||
/* 柱状图特殊样式 - 支持横向滚动 */
|
||||
.chartsMain.bar-chart {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
/* l-echart组件样式 */
|
||||
.chartsMain :deep(.l-echart) {
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
min-height: 300px !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* 柱状图横向滚动时的样式 */
|
||||
.chartsMain.bar-chart :deep(.l-echart) {
|
||||
min-width: 800rpx;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
.table-section {
|
||||
padding: 30rpx 0;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
background: var(--surface);
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow);
|
||||
border: 1rpx solid var(--border-light);
|
||||
}
|
||||
|
||||
.table-header {
|
||||
background: var(--gray-lighter);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: flex;
|
||||
border-bottom: 1rpx solid var(--border-light);
|
||||
}
|
||||
|
||||
.table-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.table-cell {
|
||||
flex: 1;
|
||||
padding: 24rpx 16rpx;
|
||||
font-size: 26rpx;
|
||||
color: var(--text-color);
|
||||
text-align: center;
|
||||
border-right: 1rpx solid var(--border-light);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.table-cell:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.header-cell {
|
||||
font-weight: 600;
|
||||
color: var(--title-color);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- 移动设备顶部状态栏 -->
|
||||
<view class="top_bar flex w-full" v-if="isMobile">
|
||||
<view class="sub-header">
|
||||
<view class="header-left" @click="goBack">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
</view>
|
||||
<view class="header-title">{{ title }}</view>
|
||||
<view class="header-right" @click="showMoreOptions">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 浏览器环境下的导航栏 -->
|
||||
<view class="sub-header" v-else>
|
||||
<view class="header-left" @click="goBack">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
</view>
|
||||
<view class="header-title">{{ title }}</view>
|
||||
<view class="header-right" @click="showMoreOptions">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SubHeader',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: '页面标题'
|
||||
},
|
||||
showBack: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showMore: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 从全局数据获取设备信息
|
||||
isMobile() {
|
||||
return getApp().globalData.isMobile;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 返回
|
||||
*/
|
||||
goBack() {
|
||||
this.$emit('goBack');
|
||||
},
|
||||
/**
|
||||
* 显示更多选项
|
||||
*/
|
||||
showMoreOptions() {
|
||||
this.$emit('showMoreOptions');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 移动设备顶部状态栏 */
|
||||
.top_bar {
|
||||
background: var(--gradient-primary);
|
||||
box-shadow: var(--shadow-lg);
|
||||
z-index: 9999;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: calc(var(--status-bar-height) + 88rpx);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding-top: var(--status-bar-height);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 支持安全区域的设备 */
|
||||
@supports (padding: max(0px)) {
|
||||
.top_bar {
|
||||
height: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
|
||||
padding-top: calc(var(--status-bar-height) + env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
.top_bar + .header {
|
||||
margin-top: calc(
|
||||
var(--status-bar-height) + 88rpx + env(safe-area-inset-top)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* 顶部导航栏 */
|
||||
.sub-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 88rpx;
|
||||
background-color: var(--surface);
|
||||
border-bottom: 1rpx solid var(--border);
|
||||
padding: 0 20rpx;
|
||||
box-sizing: border-box;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
/* 浏览器环境下的固定定位 */
|
||||
.sub-header:not(.top_bar .sub-header) {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 9999;
|
||||
height: calc(var(--status-bar-height) + 88rpx);
|
||||
padding-top: var(--status-bar-height);
|
||||
}
|
||||
|
||||
/* 移动设备下的导航栏样式 */
|
||||
.top_bar .sub-header {
|
||||
background: transparent;
|
||||
border-bottom: none;
|
||||
box-shadow: none;
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
padding: 0 20rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.top_bar .header-title {
|
||||
color: var(--white);
|
||||
font-weight: 600;
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.top_bar .header-left i,
|
||||
.top_bar .header-right i {
|
||||
color: var(--white);
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.header-left,
|
||||
.header-right {
|
||||
width: 80rpx;
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.header-right:active {
|
||||
background-color: var(--surface-hover);
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
/* 移动设备下的按钮悬停效果 */
|
||||
.top_bar .header-left:active,
|
||||
.top_bar .header-right:active {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--title-color);
|
||||
}
|
||||
|
||||
/* 浏览器环境下为内容添加顶部间距 */
|
||||
.sub-header:not(.top_bar .sub-header) + .header {
|
||||
margin-top: calc(var(--status-bar-height) + 88rpx);
|
||||
}
|
||||
|
||||
@supports (padding: max(0px)) {
|
||||
.sub-header:not(.top_bar .sub-header) {
|
||||
height: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
|
||||
padding-top: calc(var(--status-bar-height) + env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
.sub-header:not(.top_bar .sub-header) + .header {
|
||||
margin-top: calc(
|
||||
var(--status-bar-height) + 88rpx + env(safe-area-inset-top)
|
||||
);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,553 @@
|
||||
<template>
|
||||
<view class="function-page">
|
||||
<!-- 统一顶部导航 -->
|
||||
<view class="unified-header">
|
||||
<view class="header-content">
|
||||
<view class="header-left">
|
||||
<!-- <i class="fas fa-search header-icon" @click="handleSearch"></i> -->
|
||||
</view>
|
||||
<view class="header-title">功能</view>
|
||||
<view class="header-right">
|
||||
<!-- <i class="fas fa-bell header-icon" @click="handleNotification">
|
||||
<view class="badge" v-if="unreadCount > 0">{{ unreadCount }}</view>
|
||||
</i> -->
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最近使用 -->
|
||||
<view class="recent-section" v-if="recentFunctions.length > 0">
|
||||
<view class="section-title">最近使用</view>
|
||||
<view class="recent-functions">
|
||||
<view
|
||||
class="recent-item"
|
||||
v-for="(func, index) in recentFunctions"
|
||||
:key="index"
|
||||
@click="handleFunction(func)"
|
||||
>
|
||||
<view class="recent-icon">
|
||||
<i
|
||||
class="icon-text"
|
||||
:class="func.icon"
|
||||
:style="{ color: func.color }"
|
||||
></i>
|
||||
</view>
|
||||
<text class="recent-label">{{ func.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分类导航 -->
|
||||
<view class="category-tabs">
|
||||
<scroll-view scroll-x class="tabs-scroll">
|
||||
<view class="tabs">
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: activeCategory === category.key }"
|
||||
v-for="category in categories"
|
||||
:key="category.key"
|
||||
@click="switchCategory(category.key)"
|
||||
>
|
||||
<text>{{ category.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 功能列表 -->
|
||||
<scroll-view scroll-y class="unified-content">
|
||||
<view class="function-list">
|
||||
<view
|
||||
class="function-group"
|
||||
v-for="(group, groupIndex) in currentFunctions"
|
||||
:key="groupIndex"
|
||||
>
|
||||
<view class="group-functions">
|
||||
<view
|
||||
class="function-item"
|
||||
v-for="(func, funcIndex) in group.functions"
|
||||
:key="funcIndex"
|
||||
@click="handleFunction(func)"
|
||||
>
|
||||
<view class="function-icon">
|
||||
<i
|
||||
class="icon-text"
|
||||
:class="func.icon"
|
||||
:style="{ color: func.color }"
|
||||
></i>
|
||||
</view>
|
||||
<view class="function-content">
|
||||
<text class="function-name">{{ func.name }}</text>
|
||||
<text class="function-desc">{{ func.description }}</text>
|
||||
</view>
|
||||
<view class="function-arrow">
|
||||
<text class="arrow-icon">›</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, computed } from "vue";
|
||||
|
||||
// 图标
|
||||
const FA = {
|
||||
clock: "fas fa-clock",
|
||||
route: "fas fa-route",
|
||||
calendar: "fas fa-calendar-alt",
|
||||
tasks: "fas fa-tasks",
|
||||
sync: "fas fa-sync-alt",
|
||||
money: "fas fa-money-bill-wave",
|
||||
file: "fas fa-file-alt",
|
||||
userFriends: "fas fa-user-friends",
|
||||
folder: "fas fa-folder-open",
|
||||
chatBar: "far fa-chart-bar",
|
||||
};
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
// 响应式数据
|
||||
const activeCategory = ref("datas");
|
||||
const unreadCount = ref(2);
|
||||
|
||||
// 分类数据
|
||||
const categories = reactive([
|
||||
{ key: "datas", label: "业务数据" },
|
||||
{ key: "hr", label: "人事管理" },
|
||||
{ key: "finance", label: "财务报销" },
|
||||
{ key: "work", label: "工作协同" },
|
||||
{ key: "resource", label: "资源管理" },
|
||||
]);
|
||||
|
||||
// 最近使用功能
|
||||
const recentFunctions = reactive([
|
||||
{ icon: FA.calendar, color: "#FF6B6B", label: "请假", action: "leave" },
|
||||
{
|
||||
icon: FA.money,
|
||||
color: "#4ECDC4",
|
||||
label: "报销",
|
||||
action: "reimbursement",
|
||||
},
|
||||
{ icon: FA.route, color: "#45B7D1", label: "打卡", action: "checkin" },
|
||||
]);
|
||||
|
||||
// 功能数据
|
||||
const functionData = reactive({
|
||||
datas: [
|
||||
{
|
||||
functions: [
|
||||
{
|
||||
name: "业务数据",
|
||||
description: "查看业务数据",
|
||||
icon: FA.chatBar,
|
||||
color: "#0081ff",
|
||||
action: "businessData",
|
||||
},
|
||||
{
|
||||
name: "业务审批",
|
||||
description: "审批相关业务流程",
|
||||
icon: "fas fa-check-circle",
|
||||
color: "#0081ff",
|
||||
action: "approval",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
hr: [
|
||||
{
|
||||
functions: [
|
||||
{
|
||||
name: "考勤打卡",
|
||||
description: "支持定位打卡、WiFi打卡",
|
||||
icon: FA.route,
|
||||
color: "#0081ff",
|
||||
action: "attendance",
|
||||
},
|
||||
{
|
||||
name: "请假申请",
|
||||
description: "选择类型、日期、理由,附附件",
|
||||
icon: FA.calendar,
|
||||
color: "#0081ff",
|
||||
action: "leave",
|
||||
},
|
||||
{
|
||||
name: "加班申请",
|
||||
description: "申请加班,记录加班时长",
|
||||
icon: FA.tasks,
|
||||
color: "#0081ff",
|
||||
action: "overtime",
|
||||
},
|
||||
{
|
||||
name: "调休记录",
|
||||
description: "查看调休申请和记录",
|
||||
icon: FA.sync,
|
||||
color: "#0081ff",
|
||||
action: "compensatory",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
finance: [
|
||||
{
|
||||
functions: [
|
||||
{
|
||||
name: "报销提交",
|
||||
description: "选择报销类型、上传发票、填写金额",
|
||||
icon: FA.money,
|
||||
color: "#0081ff",
|
||||
action: "reimbursement",
|
||||
},
|
||||
{
|
||||
name: "报销进度",
|
||||
description: "查看报销申请进度",
|
||||
icon: FA.file,
|
||||
color: "#0081ff",
|
||||
action: "reimbursement-progress",
|
||||
},
|
||||
{
|
||||
name: "报销历史",
|
||||
description: "查看历史报销记录",
|
||||
icon: FA.tasks,
|
||||
color: "#0081ff",
|
||||
action: "reimbursement-history",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
work: [
|
||||
{
|
||||
functions: [
|
||||
{
|
||||
name: "待办任务",
|
||||
description: "关联项目、设置截止时间",
|
||||
icon: FA.tasks,
|
||||
color: "#0081ff",
|
||||
action: "todo",
|
||||
},
|
||||
{
|
||||
name: "已办任务",
|
||||
description: "查看已完成的任务",
|
||||
icon: FA.tasks,
|
||||
color: "#0081ff",
|
||||
action: "completed",
|
||||
},
|
||||
{
|
||||
name: "会议预订",
|
||||
description: "选择会议室、邀请参会人",
|
||||
icon: FA.calendar,
|
||||
color: "#0081ff",
|
||||
action: "meeting",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
resource: [
|
||||
{
|
||||
functions: [
|
||||
{
|
||||
name: "客户目录",
|
||||
description: "按行业、区域分类,显示联系方式",
|
||||
icon: FA.userFriends,
|
||||
color: "#0081ff",
|
||||
action: "customer",
|
||||
},
|
||||
{
|
||||
name: "企业文件库",
|
||||
description: "按部门权限查看,支持在线预览",
|
||||
icon: FA.folder,
|
||||
color: "#0081ff",
|
||||
action: "files",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// 计算当前分类的功能
|
||||
const currentFunctions = computed(() => {
|
||||
return functionData[activeCategory.value] || [];
|
||||
});
|
||||
|
||||
// 方法
|
||||
const handleSearch = () => {
|
||||
uni.showToast({
|
||||
title: "搜索功能",
|
||||
});
|
||||
};
|
||||
|
||||
//跳转通知页面
|
||||
const handleNotification = () => {
|
||||
uni.switchTab({
|
||||
url: "/pages/message/message",
|
||||
});
|
||||
};
|
||||
|
||||
const switchCategory = (category) => {
|
||||
activeCategory.value = category;
|
||||
};
|
||||
|
||||
const handleFunction = (func) => {
|
||||
//跳转业务数据页面
|
||||
if (func.action === "businessData") {
|
||||
uni.navigateTo({
|
||||
url: "/pages/businessDatas/index",
|
||||
});
|
||||
return;
|
||||
}
|
||||
//跳转到业务审批界面
|
||||
if (func.action === "approval") {
|
||||
uni.navigateTo({
|
||||
url: "/pages/tasks/approval",
|
||||
});
|
||||
return;
|
||||
}
|
||||
//跳转任务页面
|
||||
if (func.action === "todo") {
|
||||
uni.navigateTo({
|
||||
url: "/pages/tasks/index",
|
||||
});
|
||||
return;
|
||||
}
|
||||
//跳转考勤页面
|
||||
if (func.action === "attendance") {
|
||||
uni.navigateTo({
|
||||
url: "/pages/hr/attendance/index",
|
||||
});
|
||||
return;
|
||||
}
|
||||
uni.showToast({
|
||||
title: `功能开发中...`,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
activeCategory,
|
||||
unreadCount,
|
||||
categories,
|
||||
recentFunctions,
|
||||
currentFunctions,
|
||||
handleSearch,
|
||||
handleNotification,
|
||||
switchCategory,
|
||||
handleFunction,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.function-page {
|
||||
height: 100vh;
|
||||
background-color: var(--background);
|
||||
position: relative;
|
||||
padding-top: calc(var(--status-bar-height) + 88rpx);
|
||||
}
|
||||
|
||||
/* 支持安全区域的设备 */
|
||||
@supports (padding: max(0px)) {
|
||||
.function-page {
|
||||
padding-top: calc(
|
||||
var(--status-bar-height) + 88rpx + env(safe-area-inset-top)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
flex: 1;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 25rpx;
|
||||
padding: 15rpx 20rpx;
|
||||
margin-right: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-placeholder {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 28rpx;
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
|
||||
.notification {
|
||||
position: relative;
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
.recent-section {
|
||||
background: var(--white);
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid var(--border-light);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.recent-functions {
|
||||
display: flex;
|
||||
gap: 30rpx;
|
||||
}
|
||||
|
||||
.recent-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.recent-icon {
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.recent-label {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.category-tabs {
|
||||
background: var(--white);
|
||||
border-bottom: 1rpx solid var(--border-light);
|
||||
}
|
||||
|
||||
.tabs-scroll {
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
padding: 0 30rpx;
|
||||
min-width: max-content;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
padding: 30rpx 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
min-width: max-content;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
color: var(--primary-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tab-item.active::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 60rpx;
|
||||
height: 4rpx;
|
||||
background-color: var(--primary-color);
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
height: calc(100vh - 300rpx);
|
||||
}
|
||||
|
||||
.function-list {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.function-group {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.group-functions {
|
||||
background: var(--white);
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.function-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid var(--border-light);
|
||||
}
|
||||
|
||||
.function-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.function-icon {
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.function-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.function-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 8rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.function-desc {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.function-arrow {
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
|
||||
.search-icon,
|
||||
.notification-icon {
|
||||
font-size: 32rpx;
|
||||
margin-right: 10rpx;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: 0rpx;
|
||||
right: 15rpx;
|
||||
background-color: var(--error);
|
||||
color: var(--white);
|
||||
font-size: 20rpx;
|
||||
padding: 2rpx 8rpx;
|
||||
border-radius: 50%;
|
||||
// min-width: 30rpx;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.icon-text {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
font-size: 32rpx;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,880 @@
|
||||
<template>
|
||||
<view class="statistics-container">
|
||||
<!-- 使用子头部组件 -->
|
||||
<sub-header
|
||||
title="考勤统计"
|
||||
@goBack="goBack"
|
||||
@showMoreOptions="showMoreOptions"
|
||||
/>
|
||||
|
||||
<!-- 页面内容 -->
|
||||
<scroll-view scroll-y class="statistics-content">
|
||||
|
||||
<!-- 导出报表 -->
|
||||
<view class="export-card card">
|
||||
<view class="export-title">导出报表</view>
|
||||
<view class="exports">
|
||||
<view class="btn-export" @click="exportReport"> 导出 </view>
|
||||
<i class="fas fa-angle-right" style="font-size: 20rpx; color: var(--tab-inactive);position: relative;top: 4rpx;"></i>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 统计数据 -->
|
||||
<view class="stats-card card">
|
||||
<!-- Tabbar -->
|
||||
<view class="stat-tabs">
|
||||
<view
|
||||
v-for="t in tabs"
|
||||
:key="t.value"
|
||||
class="stat-tab"
|
||||
:class="{ active: statTab === t.value }"
|
||||
@click="statTab = t.value"
|
||||
>{{ t.label }}</view
|
||||
>
|
||||
</view>
|
||||
|
||||
<!-- 日统计(显示日历) -->
|
||||
<view v-if="statTab === 'day'" class="tab-content">
|
||||
<view class="calendar">
|
||||
<view class="cal-title">
|
||||
{{ today.getFullYear() }}年{{ today.getMonth() + 1 }}月
|
||||
</view>
|
||||
<view class="cal-week-head">
|
||||
<text
|
||||
v-for="w in ['日', '一', '二', '三', '四', '五', '六']"
|
||||
:key="w"
|
||||
>{{ w }}</text
|
||||
>
|
||||
</view>
|
||||
<view class="cal-body">
|
||||
<view
|
||||
v-for="(week, weekIndex) in calendarWeeks"
|
||||
:key="weekIndex"
|
||||
class="cal-row"
|
||||
>
|
||||
<view
|
||||
v-for="day in week"
|
||||
:key="day.key"
|
||||
class="cal-cell"
|
||||
:class="{
|
||||
empty: day.empty,
|
||||
selected:
|
||||
!day.empty && calendarSelected.includes(day.fullDate),
|
||||
}"
|
||||
@click="
|
||||
!day.empty &&
|
||||
selectDay({
|
||||
fullDate: day.fullDate,
|
||||
day: day.day,
|
||||
month: day.month,
|
||||
year: day.year,
|
||||
})
|
||||
"
|
||||
>{{ day.empty ? "" : day.day }}</view
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 选中日期的数据显示卡片 -->
|
||||
<view v-if="selectedDayInfo" class="day-data-card">
|
||||
<view class="day-data-header">
|
||||
<view class="day-data-title">
|
||||
<i class="fas fa-calendar-day"></i>
|
||||
<text>{{ selectedDayInfo.year }}年{{ selectedDayInfo.month }}月{{ selectedDayInfo.day }}日</text>
|
||||
</view>
|
||||
<view class="day-data-subtitle">考勤详情</view>
|
||||
</view>
|
||||
|
||||
<view class="day-data-content">
|
||||
<view
|
||||
class="day-data-item"
|
||||
v-for="(item, index) in dailyStats"
|
||||
:key="index"
|
||||
>
|
||||
<view class="day-data-icon" :class="item.status || 'default'">
|
||||
<i :class="item.icon"></i>
|
||||
</view>
|
||||
<view class="day-data-info">
|
||||
<view class="day-data-label">{{ item.title }}</view>
|
||||
<view class="day-data-value" :class="item.status || 'default'">
|
||||
{{ item.value }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 周统计 -->
|
||||
<view v-if="statTab === 'week'" class="tab-content">
|
||||
<view class="bar-tabs">
|
||||
<view
|
||||
class="bar-tab"
|
||||
v-for="(item, index) in weekTabs"
|
||||
:key="item.value"
|
||||
:class="{ active: weekTab === item.value }"
|
||||
@click="weekTab = item.value"
|
||||
>{{ item.label }}</view
|
||||
>
|
||||
</view>
|
||||
<view class="count-list">
|
||||
<view
|
||||
class="count-item"
|
||||
v-for="(item, idx) in (weekStats[weekTab] || [])"
|
||||
:key="idx"
|
||||
>
|
||||
<view class="c-value">{{ item.value }}</view>
|
||||
<view class="c-title">{{ item.title }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 月统计 -->
|
||||
<view v-if="statTab === 'month'" class="tab-content">
|
||||
<view class="month-grid">
|
||||
<view
|
||||
class="month-item"
|
||||
v-for="(item, index) in monthTabs"
|
||||
:key="item.value"
|
||||
:class="{ active: monthTab === item.value }"
|
||||
@click="monthTab = item.value"
|
||||
>
|
||||
<view class="month-label">{{ item.label }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="count-list">
|
||||
<view
|
||||
class="count-item"
|
||||
v-for="(item, idx) in (monthStats[monthTab] || [])"
|
||||
:key="idx"
|
||||
>
|
||||
<view class="c-value">{{ item.value }}</view>
|
||||
<view class="c-title">{{ item.title }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SubHeader from '../../components/subHeader.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SubHeader
|
||||
},
|
||||
data() {
|
||||
const today = new Date();
|
||||
return {
|
||||
today: today,
|
||||
tabs: [
|
||||
{ label: "日统计", value: "day" },
|
||||
{ label: "周统计", value: "week" },
|
||||
{ label: "月统计", value: "month" },
|
||||
],
|
||||
statTab: "day",
|
||||
calendarSelected: [
|
||||
`${today.getFullYear()}-${String(today.getMonth() + 1).padStart(
|
||||
2,
|
||||
"0"
|
||||
)}-${String(today.getDate()).padStart(2, "0")}`,
|
||||
],
|
||||
weekTabs: [
|
||||
{ label: "第1周", value: "1" },
|
||||
{ label: "第2周", value: "2" },
|
||||
{ label: "第3周", value: "3" },
|
||||
{ label: "第4周", value: "4" },
|
||||
],
|
||||
weekTab: "1",
|
||||
monthTabs: Array.from({ length: 12 }, (_, i) => ({
|
||||
label: `${i + 1}月`,
|
||||
value: String(i + 1),
|
||||
})),
|
||||
monthTab: String(today.getMonth() + 1),
|
||||
baseStat: [
|
||||
{ title: "平均工时", value: "7.8h" },
|
||||
{ title: "迟到次数", value: "1" },
|
||||
{ title: "早退次数", value: "0" },
|
||||
{ title: "缺卡次数", value: "0" },
|
||||
{ title: "旷工次数", value: "0" },
|
||||
{ title: "外勤次数", value: "2" },
|
||||
{ title: "加班时长", value: "4h" },
|
||||
{ title: "调休时长", value: "2h" },
|
||||
],
|
||||
selectedDayData: null, // 选中日期的数据
|
||||
selectedDayInfo: null, // 选中日期的基本信息
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
weekStats() {
|
||||
try {
|
||||
return {
|
||||
"1": this.baseStat || [],
|
||||
"2": (this.baseStat || []).map((item, i) =>
|
||||
i === 1 ? { ...item, value: "0" } : item
|
||||
),
|
||||
"3": (this.baseStat || []).map((item, i) =>
|
||||
i === 3 ? { ...item, value: "1" } : item
|
||||
),
|
||||
"4": (this.baseStat || []).map((item, i) =>
|
||||
i === 5 ? { ...item, value: "3" } : item
|
||||
),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('周统计数据生成错误:', error);
|
||||
return {};
|
||||
}
|
||||
},
|
||||
monthStats() {
|
||||
try {
|
||||
const stats = {};
|
||||
(this.monthTabs || []).forEach((_, i) => {
|
||||
stats[String(i + 1)] = (this.baseStat || []).map((item) => ({
|
||||
...item,
|
||||
value: String(
|
||||
Math.floor(Math.random() * 3) +
|
||||
(item.value.includes("h") ? "h" : "")
|
||||
),
|
||||
}));
|
||||
});
|
||||
return stats;
|
||||
} catch (error) {
|
||||
console.error('月统计数据生成错误:', error);
|
||||
return {};
|
||||
}
|
||||
},
|
||||
// 日历相关计算属性
|
||||
days() {
|
||||
const d = new Date(
|
||||
this.today.getFullYear(),
|
||||
this.today.getMonth() + 1,
|
||||
0
|
||||
).getDate();
|
||||
return Array.from({ length: d }, (_, i) => i + 1);
|
||||
},
|
||||
firstDay() {
|
||||
return new Date(
|
||||
this.today.getFullYear(),
|
||||
this.today.getMonth(),
|
||||
1
|
||||
).getDay();
|
||||
},
|
||||
calendarWeeks() {
|
||||
try {
|
||||
const year = this.today.getFullYear();
|
||||
const month = this.today.getMonth();
|
||||
const firstDay = new Date(year, month, 1).getDay();
|
||||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||||
|
||||
const weeks = [];
|
||||
let currentWeek = [];
|
||||
|
||||
// 添加空白日期(上个月的末尾几天)
|
||||
for (let i = 0; i < firstDay; i++) {
|
||||
currentWeek.push({
|
||||
key: `empty-${i}`,
|
||||
empty: true,
|
||||
day: "",
|
||||
fullDate: "",
|
||||
month: month,
|
||||
year: year,
|
||||
});
|
||||
}
|
||||
|
||||
// 添加当前月的日期
|
||||
for (let day = 1; day <= daysInMonth; day++) {
|
||||
const fullDate = `${year}-${String(month + 1).padStart(
|
||||
2,
|
||||
"0"
|
||||
)}-${String(day).padStart(2, "0")}`;
|
||||
currentWeek.push({
|
||||
key: `day-${day}`,
|
||||
empty: false,
|
||||
day: day,
|
||||
fullDate: fullDate,
|
||||
month: month + 1,
|
||||
year: year,
|
||||
});
|
||||
|
||||
// 如果一周满了(7天),开始新的一周
|
||||
if (currentWeek.length === 7) {
|
||||
weeks.push([...currentWeek]);
|
||||
currentWeek = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 如果最后一周不满7天,用空白日期填充
|
||||
while (currentWeek.length > 0 && currentWeek.length < 7) {
|
||||
currentWeek.push({
|
||||
key: `empty-end-${currentWeek.length}`,
|
||||
empty: true,
|
||||
day: "",
|
||||
fullDate: "",
|
||||
month: month,
|
||||
year: year,
|
||||
});
|
||||
}
|
||||
|
||||
// 如果还有未完成的周,添加到结果中
|
||||
if (currentWeek.length > 0) {
|
||||
weeks.push(currentWeek);
|
||||
}
|
||||
|
||||
return weeks;
|
||||
} catch (error) {
|
||||
console.error('日历生成错误:', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
isMobile() {
|
||||
return getApp().globalData.isMobile;
|
||||
},
|
||||
// 生成每日数据
|
||||
dailyStats() {
|
||||
if (!this.selectedDayInfo) return [];
|
||||
|
||||
const { year, month, day } = this.selectedDayInfo;
|
||||
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||
|
||||
// 模拟不同日期的不同数据
|
||||
const randomSeed = year * 10000 + month * 100 + day;
|
||||
const random = (seed) => {
|
||||
const x = Math.sin(seed) * 10000;
|
||||
return x - Math.floor(x);
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
title: "上班时间",
|
||||
value: `${String(Math.floor(random(randomSeed) * 2) + 8).padStart(2, '0')}:${String(Math.floor(random(randomSeed + 1) * 60)).padStart(2, '0')}`,
|
||||
icon: "fas fa-clock"
|
||||
},
|
||||
{
|
||||
title: "下班时间",
|
||||
value: `${String(Math.floor(random(randomSeed + 2) * 2) + 17).padStart(2, '0')}:${String(Math.floor(random(randomSeed + 3) * 60)).padStart(2, '0')}`,
|
||||
icon: "fas fa-clock"
|
||||
},
|
||||
{
|
||||
title: "迟到状态",
|
||||
value: random(randomSeed + 5) > 0.8 ? "迟到" : "正常",
|
||||
icon: random(randomSeed + 5) > 0.8 ? "fas fa-exclamation-triangle" : "fas fa-check-circle",
|
||||
status: random(randomSeed + 5) > 0.8 ? "warning" : "success"
|
||||
},
|
||||
{
|
||||
title: "早退状态",
|
||||
value: random(randomSeed + 6) > 0.9 ? "早退" : "正常",
|
||||
icon: random(randomSeed + 6) > 0.9 ? "fas fa-exclamation-triangle" : "fas fa-check-circle",
|
||||
status: random(randomSeed + 6) > 0.9 ? "warning" : "success"
|
||||
},
|
||||
{
|
||||
title: "工作时长",
|
||||
value: `${(random(randomSeed + 4) * 4 + 6).toFixed(1)}h`,
|
||||
icon: "fas fa-hourglass-half"
|
||||
},
|
||||
{
|
||||
title: "外勤次数",
|
||||
value: Math.floor(random(randomSeed + 7) * 3).toString(),
|
||||
icon: "fas fa-map-marker-alt"
|
||||
},
|
||||
{
|
||||
title: "加班时长",
|
||||
value: random(randomSeed + 8) > 0.6 ? `${(random(randomSeed + 8) * 3).toFixed(1)}h` : "0h",
|
||||
icon: "fas fa-moon"
|
||||
},
|
||||
{
|
||||
title: "调休时长",
|
||||
value: random(randomSeed + 9) > 0.7 ? `${(random(randomSeed + 9) * 2).toFixed(1)}h` : "0h",
|
||||
icon: "fas fa-calendar-alt"
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
console.log('统计页面初始化');
|
||||
console.log('当前月份:', this.today.getMonth() + 1);
|
||||
console.log('基础数据:', this.baseStat);
|
||||
console.log('周标签:', this.weekTabs);
|
||||
console.log('月标签:', this.monthTabs);
|
||||
console.log('当前周标签值:', this.weekTab, typeof this.weekTab);
|
||||
console.log('当前月标签值:', this.monthTab, typeof this.monthTab);
|
||||
console.log('周统计数据:', this.weekStats);
|
||||
console.log('月统计数据:', this.monthStats);
|
||||
console.log('周统计键:', Object.keys(this.weekStats));
|
||||
console.log('月统计键:', Object.keys(this.monthStats));
|
||||
console.log('周统计访问测试:', this.weekStats[this.weekTab]);
|
||||
console.log('月统计访问测试:', this.monthStats[this.monthTab]);
|
||||
|
||||
// 初始化默认选中今天
|
||||
this.selectedDayInfo = {
|
||||
year: this.today.getFullYear(),
|
||||
month: this.today.getMonth() + 1,
|
||||
day: this.today.getDate(),
|
||||
fullDate: `${this.today.getFullYear()}-${String(this.today.getMonth() + 1).padStart(2, '0')}-${String(this.today.getDate()).padStart(2, '0')}`
|
||||
};
|
||||
console.log('默认选中今天:', this.selectedDayInfo);
|
||||
},
|
||||
methods: {
|
||||
selectDay(day) {
|
||||
this.calendarSelected = [day.fullDate];
|
||||
this.selectedDayInfo = {
|
||||
year: day.year,
|
||||
month: day.month,
|
||||
day: day.day,
|
||||
fullDate: day.fullDate
|
||||
};
|
||||
console.log('选择日期:', day);
|
||||
console.log('选中日期信息:', this.selectedDayInfo);
|
||||
},
|
||||
exportReport() {
|
||||
uni.showToast({ title: "导出功能暂未开放" });
|
||||
},
|
||||
goBack() {
|
||||
uni.navigateBack();
|
||||
},
|
||||
showMoreOptions() {
|
||||
uni.showActionSheet({
|
||||
itemList: ['刷新数据', '导出报表', '设置'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) {
|
||||
uni.showToast({ title: '刷新成功' });
|
||||
} else if (res.tapIndex === 1) {
|
||||
this.exportReport();
|
||||
} else if (res.tapIndex === 2) {
|
||||
uni.showToast({ title: '设置功能暂未开放' });
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.statistics-container {
|
||||
background: #f7f8fa;
|
||||
min-height: 100vh;
|
||||
padding-top: calc(var(--status-bar-height) + 88rpx);
|
||||
}
|
||||
|
||||
/* 支持安全区域的设备 */
|
||||
@supports (padding: max(0px)) {
|
||||
.statistics-container {
|
||||
padding-top: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
|
||||
}
|
||||
}
|
||||
|
||||
.statistics-content {
|
||||
padding: 32rpx 0;
|
||||
min-height: calc(100vh - 88rpx);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 4rpx 16rpx 0 rgba(0, 0, 0, 0.04);
|
||||
margin: 0 32rpx 32rpx;
|
||||
padding: 32rpx;
|
||||
}
|
||||
.export-card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.export-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
.btn-export {
|
||||
// background: #3c9cff;
|
||||
color: var(--tab-inactive);
|
||||
font-size: 28rpx;
|
||||
border: none;
|
||||
// padding: 4rpx 40rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.exports {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.btn-export {
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
.stats-card {
|
||||
.stat-tabs {
|
||||
display: flex;
|
||||
margin-bottom: 32rpx;
|
||||
.stat-tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: var(--text-muted);
|
||||
padding-bottom: 16rpx;
|
||||
border-bottom: 4rpx solid transparent;
|
||||
transition: all 0.2s;
|
||||
&.active {
|
||||
color: var(--primary-color);
|
||||
font-weight: 600;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
.tab-content {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
// 月统计网格布局
|
||||
.month-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 12rpx;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.month-item {
|
||||
background: var(--gray-lighter);
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx 12rpx;
|
||||
text-align: center;
|
||||
border: 1rpx solid var(--border-light);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
|
||||
.month-label {
|
||||
font-size: 26rpx;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: translateY(-2rpx);
|
||||
|
||||
.month-label {
|
||||
color: var(--white);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.active):active {
|
||||
background: var(--primary-light);
|
||||
transform: scale(0.98);
|
||||
|
||||
.month-label {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bar-tabs {
|
||||
display: flex;
|
||||
background: var(--gray-lighter);
|
||||
border-radius: 12rpx;
|
||||
padding: 6rpx;
|
||||
margin-bottom: 24rpx;
|
||||
position: relative;
|
||||
|
||||
.bar-tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: var(--text-secondary);
|
||||
padding: 16rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
margin: 0 2rpx;
|
||||
background: transparent;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
font-weight: 500;
|
||||
|
||||
&.active {
|
||||
color: var(--primary-color);
|
||||
background: var(--surface);
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-1rpx);
|
||||
}
|
||||
|
||||
&:not(.active):active {
|
||||
background: rgba(60, 156, 255, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bar-tabs-scroll {
|
||||
background: #f8f9fa;
|
||||
border-radius: 12rpx;
|
||||
padding: 6rpx;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.bar-scroll {
|
||||
white-space: nowrap;
|
||||
|
||||
.bar-tab {
|
||||
display: inline-block;
|
||||
min-width: 120rpx;
|
||||
margin: 0 4rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
border-radius: 8rpx;
|
||||
background: transparent;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 500;
|
||||
|
||||
&.active {
|
||||
color: #3c9cff;
|
||||
background: #fff;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-1rpx);
|
||||
}
|
||||
|
||||
&:not(.active):active {
|
||||
background: rgba(60, 156, 255, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 日统计数据显示卡片
|
||||
.day-data-card {
|
||||
margin-top: 24rpx;
|
||||
background: var(--surface);
|
||||
border-radius: 16rpx;
|
||||
box-shadow: var(--shadow-md);
|
||||
overflow: hidden;
|
||||
border: 1rpx solid var(--border);
|
||||
|
||||
.day-data-header {
|
||||
background: var(--gradient-primary);
|
||||
padding: 32rpx 24rpx;
|
||||
color: var(--white);
|
||||
|
||||
.day-data-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8rpx;
|
||||
color: var(--white);
|
||||
|
||||
i {
|
||||
margin-right: 12rpx;
|
||||
font-size: 28rpx;
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
.day-data-subtitle {
|
||||
font-size: 24rpx;
|
||||
opacity: 0.9;
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
.day-data-content {
|
||||
padding: 24rpx;
|
||||
background: var(--surface);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
|
||||
.day-data-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx 16rpx;
|
||||
width: 48%;
|
||||
margin-bottom: 16rpx;
|
||||
background: var(--gray-lighter);
|
||||
border-radius: 12rpx;
|
||||
border: 1rpx solid var(--border-light);
|
||||
box-sizing: border-box;
|
||||
|
||||
&:nth-child(odd) {
|
||||
margin-right: 2%;
|
||||
}
|
||||
|
||||
&:nth-child(even) {
|
||||
margin-left: 2%;
|
||||
}
|
||||
|
||||
.day-data-icon {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12rpx;
|
||||
flex-shrink: 0;
|
||||
|
||||
i {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
&.default {
|
||||
background: var(--gray-light);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
&.success {
|
||||
background: var(--success-light);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background: var(--warning-light);
|
||||
color: var(--warning);
|
||||
}
|
||||
}
|
||||
|
||||
.day-data-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.day-data-label {
|
||||
font-size: 22rpx;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 6rpx;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.day-data-value {
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
|
||||
&.default {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
&.success {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: var(--warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.count-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 16rpx;
|
||||
justify-content: space-between;
|
||||
.count-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 48%;
|
||||
margin-bottom: 16rpx;
|
||||
padding: 24rpx;
|
||||
background: var(--gray-lighter);
|
||||
border-radius: 12rpx;
|
||||
box-sizing: border-box;
|
||||
border: 1rpx solid var(--border-light);
|
||||
.c-title {
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
.c-value {
|
||||
color: var(--text-color);
|
||||
font-size: 40rpx;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 日历样式 */
|
||||
.calendar {
|
||||
background: var(--surface);
|
||||
border-radius: 12rpx;
|
||||
padding: 24rpx;
|
||||
margin-top: 16rpx;
|
||||
border: 1rpx solid var(--border);
|
||||
box-shadow: var(--shadow);
|
||||
|
||||
.cal-title {
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 32rpx;
|
||||
color: var(--title-color);
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.cal-week-head {
|
||||
display: flex;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
text {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
padding: 12rpx 0;
|
||||
}
|
||||
}
|
||||
|
||||
.cal-body {
|
||||
.cal-row {
|
||||
display: flex;
|
||||
margin-bottom: 8rpx;
|
||||
|
||||
.cal-cell {
|
||||
flex: 1;
|
||||
height: 64rpx;
|
||||
line-height: 64rpx;
|
||||
text-align: center;
|
||||
border-radius: 8rpx;
|
||||
margin: 0 4rpx;
|
||||
background: var(--gray-lighter);
|
||||
font-size: 28rpx;
|
||||
color: var(--text-color);
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: var(--primary-color);
|
||||
color: var(--white);
|
||||
font-weight: 600;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
&.empty {
|
||||
background: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:not(.empty):not(.selected):hover {
|
||||
background: var(--primary-light);
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep() {
|
||||
.uni-scroll-view {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,927 @@
|
||||
<template>
|
||||
<view class="login-page">
|
||||
<!-- 背景装饰 -->
|
||||
<view class="bg-decoration">
|
||||
<view class="gradient-orb orb-1"></view>
|
||||
<view class="gradient-orb orb-2"></view>
|
||||
<view class="gradient-orb orb-3"></view>
|
||||
<view class="floating-shapes">
|
||||
<view class="shape shape-1"></view>
|
||||
<view class="shape shape-2"></view>
|
||||
<view class="shape shape-3"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<view class="login-container">
|
||||
<!-- 头部区域 -->
|
||||
<view class="header-section">
|
||||
<view class="logo-container">
|
||||
<view class="logo-wrapper">
|
||||
<image src="/static/logo.png" class="logo" mode="aspectFit"></image>
|
||||
<view class="logo-ring"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="welcome-content">
|
||||
<text class="app-title">企业办公系统</text>
|
||||
<text class="welcome-subtitle">欢迎回来,开始您的工作之旅</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 登录卡片 -->
|
||||
<view class="login-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">登录账户</text>
|
||||
<view class="card-subtitle">请输入您的登录信息</view>
|
||||
</view>
|
||||
|
||||
<view class="form-container">
|
||||
<!-- 用户名输入 -->
|
||||
<view class="input-field-group">
|
||||
<view class="input-container" :class="{ 'focused': usernameFocused, 'error': usernameError }">
|
||||
<view class="input-icon-wrapper">
|
||||
<i class="fas fa-user input-icon"></i>
|
||||
</view>
|
||||
<input
|
||||
v-model="form.username"
|
||||
placeholder="用户名"
|
||||
class="input"
|
||||
type="text"
|
||||
@focus="handleUsernameFocus"
|
||||
@blur="handleUsernameBlur"
|
||||
@input="clearUsernameError">
|
||||
<view class="input-border"></view>
|
||||
</view>
|
||||
<text class="error-text" v-if="usernameError">{{ usernameError }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 密码输入 -->
|
||||
<view class="input-field-group">
|
||||
<view class="input-container" :class="{ 'focused': passwordFocused, 'error': passwordError }">
|
||||
<view class="input-icon-wrapper">
|
||||
<i class="fas fa-lock input-icon"></i>
|
||||
</view>
|
||||
<input
|
||||
v-model="form.password"
|
||||
placeholder="密码"
|
||||
class="input"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
@focus="handlePasswordFocus"
|
||||
@blur="handlePasswordBlur"
|
||||
@input="clearPasswordError">
|
||||
<view class="password-toggle" @click="togglePassword">
|
||||
<i :class="showPassword ? 'fas fa-eye-slash' : 'fas fa-eye'"></i>
|
||||
</view>
|
||||
<view class="input-border"></view>
|
||||
</view>
|
||||
<text class="error-text" v-if="passwordError">{{ passwordError }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 选项区域 -->
|
||||
<view class="options-row">
|
||||
<view class="remember-section" @click="toggleRemember">
|
||||
<view class="custom-checkbox" :class="{ 'checked': rememberMe }">
|
||||
<i class="fas fa-check" v-if="rememberMe"></i>
|
||||
</view>
|
||||
<text class="remember-text">记住我</text>
|
||||
</view>
|
||||
<text class="forgot-link" @click="handleForgotPassword">忘记密码?</text>
|
||||
</view>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<button
|
||||
:disabled="loading || !isFormValid"
|
||||
class="login-button"
|
||||
:class="{ 'loading': loading, 'disabled': !isFormValid }"
|
||||
@click="handleLogin">
|
||||
<view class="button-content">
|
||||
<view class="button-icon" v-if="!loading">
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</view>
|
||||
<view class="loading-spinner" v-if="loading">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
</view>
|
||||
<text class="button-text">{{ loading ? '登录中...' : '立即登录' }}</text>
|
||||
</view>
|
||||
<view class="button-shine" v-if="!loading"></view>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 测试提示 -->
|
||||
<view class="test-tips">
|
||||
<view class="tips-icon">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
</view>
|
||||
<text class="tips-text">测试账号:test / 123456</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { userApi } from '../../src/api/index.js'
|
||||
import { useAuthStore } from '../../src/store/authStore.js'
|
||||
import { redirectAfterLogin } from '../../src/utils/routeGuard.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
username: '',
|
||||
password: ''
|
||||
},
|
||||
loading: false,
|
||||
usernameFocused: false,
|
||||
passwordFocused: false,
|
||||
showPassword: false,
|
||||
rememberMe: false,
|
||||
usernameError: '',
|
||||
passwordError: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isFormValid() {
|
||||
return this.form.username.trim() && this.form.password.trim()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async handleLogin() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await userApi.login(this.form);
|
||||
console.log('登录响应:', res);
|
||||
|
||||
// 根据你的后端返回结构调整
|
||||
const token = res.accessToken || res.token;
|
||||
|
||||
if (!token) {
|
||||
throw new Error('登录失败:未获取到访问令牌');
|
||||
}
|
||||
|
||||
const authStore = useAuthStore();
|
||||
// 从响应中获取用户信息
|
||||
const userInfo = res.user || {
|
||||
username: this.form.username,
|
||||
id: res.id
|
||||
};
|
||||
|
||||
authStore.login(userInfo, token);
|
||||
|
||||
uni.showToast({
|
||||
title: '登录成功',
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
// 延迟跳转,让用户看到成功提示
|
||||
setTimeout(() => {
|
||||
redirectAfterLogin();
|
||||
}, 500);
|
||||
} catch (e) {
|
||||
this.loading = false;
|
||||
console.error('登录错误:', e);
|
||||
|
||||
// 显示错误信息
|
||||
const errorMessage = e.message || e.msg || '登录失败,请检查网络连接';
|
||||
uni.showToast({
|
||||
title: errorMessage,
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// async handleLogin2() {
|
||||
// if(!this.form.username || !this.form.password) {
|
||||
// uni.showToast({
|
||||
// title: '请输入用户名和密码',
|
||||
// icon: 'none'
|
||||
// });
|
||||
// return;
|
||||
// }
|
||||
|
||||
// this.loading = true;
|
||||
|
||||
// try {
|
||||
// // 模拟登录API请求
|
||||
// // 实际项目中应该调用真实的后端API
|
||||
// const mockLogin = () => {
|
||||
// return new Promise((resolve) => {
|
||||
// setTimeout(() => {
|
||||
// // 模拟登录成功
|
||||
// if (this.form.username === 'admin' && this.form.password === '123456') {
|
||||
// resolve({
|
||||
// code: 0,
|
||||
// message: '登录成功',
|
||||
// data: {
|
||||
// token: 'mock_token_' + Date.now(),
|
||||
// userInfo: {
|
||||
// id: 1,
|
||||
// username: this.form.username,
|
||||
// name: '管理员',
|
||||
// avatar: '/static/logo.png',
|
||||
// department: '技术部',
|
||||
// role: 'admin'
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// } else {
|
||||
// resolve({
|
||||
// code: 1,
|
||||
// message: '用户名或密码错误'
|
||||
// });
|
||||
// }
|
||||
// }, 1000);
|
||||
// });
|
||||
// };
|
||||
|
||||
// const result = await mockLogin();
|
||||
|
||||
// if (result.code === 0) {
|
||||
// // 使用 Pinia store 管理登录状态
|
||||
// const authStore = useAuthStore();
|
||||
// authStore.login(result.data.userInfo, result.data.token);
|
||||
|
||||
// uni.showToast({
|
||||
// title: '登录成功',
|
||||
// icon: 'success'
|
||||
// });
|
||||
|
||||
// // 延迟跳转,让用户看到成功提示
|
||||
// setTimeout(() => {
|
||||
// redirectAfterLogin();
|
||||
// }, 500);
|
||||
// } else {
|
||||
// uni.showToast({
|
||||
// title: result.message || '登录失败',
|
||||
// icon: 'none'
|
||||
// });
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error('登录错误:', error);
|
||||
// uni.showToast({
|
||||
// title: '网络异常,请稍后重试',
|
||||
// icon: 'none'
|
||||
// });
|
||||
// }
|
||||
|
||||
// this.loading = false;
|
||||
// },
|
||||
|
||||
// 切换密码显示
|
||||
togglePassword() {
|
||||
this.showPassword = !this.showPassword
|
||||
},
|
||||
|
||||
// 切换记住我
|
||||
toggleRemember() {
|
||||
this.rememberMe = !this.rememberMe
|
||||
},
|
||||
|
||||
// 忘记密码
|
||||
handleForgotPassword() {
|
||||
uni.showToast({
|
||||
title: '请联系管理员重置密码',
|
||||
icon: 'none'
|
||||
})
|
||||
},
|
||||
|
||||
// 处理用户名输入框焦点
|
||||
handleUsernameFocus() {
|
||||
this.usernameFocused = true;
|
||||
this.clearUsernameError();
|
||||
},
|
||||
|
||||
handleUsernameBlur() {
|
||||
this.usernameFocused = false;
|
||||
this.validateUsername();
|
||||
},
|
||||
|
||||
// 处理密码输入框焦点
|
||||
handlePasswordFocus() {
|
||||
this.passwordFocused = true;
|
||||
this.clearPasswordError();
|
||||
},
|
||||
|
||||
handlePasswordBlur() {
|
||||
this.passwordFocused = false;
|
||||
this.validatePassword();
|
||||
},
|
||||
|
||||
// 清除用户名错误
|
||||
clearUsernameError() {
|
||||
this.usernameError = '';
|
||||
},
|
||||
|
||||
// 清除密码错误
|
||||
clearPasswordError() {
|
||||
this.passwordError = '';
|
||||
},
|
||||
|
||||
// 验证用户名
|
||||
validateUsername() {
|
||||
if (!this.form.username.trim()) {
|
||||
this.usernameError = '请输入用户名';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
// 验证密码
|
||||
validatePassword() {
|
||||
if (!this.form.password.trim()) {
|
||||
this.passwordError = '请输入密码';
|
||||
return false;
|
||||
}
|
||||
if (this.form.password.length < 6) {
|
||||
this.passwordError = '密码至少6位';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 主容器 */
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
background: var(--gradient-primary);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40rpx 20rpx;
|
||||
}
|
||||
|
||||
/* 背景装饰 */
|
||||
.bg-decoration {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.gradient-orb {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(45deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
|
||||
animation: float 8s ease-in-out infinite;
|
||||
filter: blur(1rpx);
|
||||
}
|
||||
|
||||
.orb-1 {
|
||||
width: 300rpx;
|
||||
height: 300rpx;
|
||||
top: -150rpx;
|
||||
left: -150rpx;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.orb-2 {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
top: 20%;
|
||||
right: -100rpx;
|
||||
animation-delay: 3s;
|
||||
}
|
||||
|
||||
.orb-3 {
|
||||
width: 150rpx;
|
||||
height: 150rpx;
|
||||
bottom: 10%;
|
||||
left: 10%;
|
||||
animation-delay: 6s;
|
||||
}
|
||||
|
||||
.floating-shapes {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.shape {
|
||||
position: absolute;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.shape-1 {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 50%;
|
||||
top: 30%;
|
||||
left: 20%;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
.shape-2 {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 8rpx;
|
||||
top: 60%;
|
||||
right: 30%;
|
||||
animation-delay: 4s;
|
||||
}
|
||||
|
||||
.shape-3 {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
bottom: 30%;
|
||||
right: 20%;
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0px) rotate(0deg) scale(1);
|
||||
opacity: 0.7;
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-30px) rotate(180deg) scale(1.1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 登录容器 */
|
||||
.login-container {
|
||||
width: 100%;
|
||||
max-width: 600rpx;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* 头部区域 */
|
||||
.header-section {
|
||||
text-align: center;
|
||||
margin-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.logo-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.2);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.logo-ring {
|
||||
position: absolute;
|
||||
top: -8rpx;
|
||||
left: -8rpx;
|
||||
right: -8rpx;
|
||||
bottom: -8rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 28rpx;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
opacity: 0.7;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.welcome-content {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
display: block;
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
margin-bottom: 16rpx;
|
||||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.welcome-subtitle {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
opacity: 0.9;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* 登录卡片 */
|
||||
.login-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 24rpx;
|
||||
padding: 50rpx 40rpx;
|
||||
box-shadow: var(--shadow-lg);
|
||||
backdrop-filter: blur(20rpx);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.2);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4rpx;
|
||||
background: var(--gradient-primary);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
text-align: center;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 8rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
font-size: 26rpx;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* 表单容器 */
|
||||
.form-container {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
/* 输入框组 */
|
||||
.input-field-group {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--gray-lighter);
|
||||
border-radius: 16rpx;
|
||||
border: 2rpx solid var(--border);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow: hidden;
|
||||
min-height: 88rpx;
|
||||
}
|
||||
|
||||
.input-container.focused {
|
||||
border-color: var(--primary-color);
|
||||
background: var(--white);
|
||||
box-shadow: 0 0 0 4rpx var(--info-light);
|
||||
transform: translateY(-2rpx);
|
||||
}
|
||||
|
||||
.input-container.error {
|
||||
border-color: var(--error);
|
||||
background: var(--error-light);
|
||||
}
|
||||
|
||||
.input-icon-wrapper {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
color: var(--text-muted);
|
||||
font-size: 28rpx;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.input-container.focused .input-icon {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.input-container.error .input-icon {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.input {
|
||||
flex: 1;
|
||||
padding: 24rpx 20rpx;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 30rpx;
|
||||
color: var(--text-color);
|
||||
outline: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.input::placeholder {
|
||||
color: var(--text-muted);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.password-toggle {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 28rpx;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s ease;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.password-toggle:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.input-border {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2rpx;
|
||||
background: var(--gradient-primary);
|
||||
transform: scaleX(0);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.input-container.focused .input-border {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.error-text {
|
||||
font-size: 24rpx;
|
||||
color: var(--error);
|
||||
margin-top: 8rpx;
|
||||
margin-left: 20rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 选项区域 */
|
||||
.options-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.remember-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.custom-checkbox {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
border: 2rpx solid var(--border);
|
||||
border-radius: 8rpx;
|
||||
margin-right: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
.custom-checkbox.checked {
|
||||
background: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.custom-checkbox i {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.remember-text {
|
||||
font-size: 28rpx;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.forgot-link {
|
||||
font-size: 28rpx;
|
||||
color: var(--primary-color);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.forgot-link:hover {
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
/* 登录按钮 */
|
||||
.login-button {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background: var(--gradient-primary);
|
||||
color: var(--white);
|
||||
border: none;
|
||||
border-radius: 16rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-lg);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.login-button:hover:not(:disabled) {
|
||||
transform: translateY(-2rpx);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.login-button:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.login-button.disabled {
|
||||
opacity: 0.5;
|
||||
transform: none;
|
||||
cursor: not-allowed;
|
||||
background: var(--gray);
|
||||
color: var(--text-muted);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.login-button.loading {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.button-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.button-text {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.button-shine {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
animation: shine 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes shine {
|
||||
0% { left: -100%; }
|
||||
100% { left: 100%; }
|
||||
}
|
||||
|
||||
/* 测试提示 */
|
||||
.test-tips {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
margin-top: 30rpx;
|
||||
padding: 20rpx;
|
||||
background: var(--info-light);
|
||||
border-radius: 12rpx;
|
||||
border: 1rpx solid var(--primary-light);
|
||||
}
|
||||
|
||||
.tips-icon {
|
||||
color: var(--primary-color);
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.tips-text {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media screen and (max-width: 750rpx) {
|
||||
.login-container {
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
padding: 40rpx 30rpx;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
font-size: 42rpx;
|
||||
}
|
||||
|
||||
.welcome-subtitle {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
/* 深色模式适配 - 保持亮色主题 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.login-page {
|
||||
background: var(--gradient-primary);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.input-container {
|
||||
background: var(--gray-lighter);
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.input-container.focused {
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
.input {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.remember-text {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.forgot-link {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.test-tips {
|
||||
background: var(--info-light);
|
||||
border-color: var(--primary-light);
|
||||
}
|
||||
|
||||
.tips-text {
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,850 @@
|
||||
<template>
|
||||
<view class="chat-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<view class="top_bar flex w-full" v-if="isMobile">
|
||||
<view class="chat-header">
|
||||
<view class="header-left" @click="goBack">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
</view>
|
||||
<view class="header-title">聊天</view>
|
||||
<view class="header-right" @click="goToChatDetail">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 浏览器环境下的导航栏 -->
|
||||
<view class="chat-header" v-else>
|
||||
<view class="header-left" @click="goBack">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
</view>
|
||||
<view class="header-title">聊天</view>
|
||||
<view class="header-right" @click="goToChatDetail">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 聊天消息列表 -->
|
||||
<scroll-view
|
||||
class="message-list"
|
||||
scroll-y
|
||||
:scroll-top="scrollTop"
|
||||
:scroll-into-view="scrollIntoView"
|
||||
>
|
||||
<block v-for="(message, index) in messages" :key="index">
|
||||
<view
|
||||
:id="'message-' + index"
|
||||
:class="[
|
||||
'message-item',
|
||||
message.type === 'sent' ? 'sent' : 'received',
|
||||
]"
|
||||
>
|
||||
<!-- 头像 -->
|
||||
<view class="avatar" @click="goToUserDetail">
|
||||
<image
|
||||
:src="
|
||||
message.type === 'sent'
|
||||
? '/static/imgs/default_avatar.png'
|
||||
: '/static/imgs/default_avatar.png'
|
||||
"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
</view>
|
||||
|
||||
<!-- 消息内容 -->
|
||||
<view class="message-content">
|
||||
<view class="message-bubble">
|
||||
<!-- 文本消息 -->
|
||||
<text class="message-text">{{
|
||||
parseEmoji(message.content)
|
||||
}}</text>
|
||||
</view>
|
||||
<!-- 消息时间 -->
|
||||
<view class="message-time" v-if="message.time">
|
||||
{{ formatTime(message.time) }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<view class="input-area">
|
||||
<!-- 表情选择器 -->
|
||||
<view class="emoji-picker-container" v-if="showEmojiPicker">
|
||||
<EmojiPicker
|
||||
:visible="showEmojiPicker"
|
||||
@select="onEmojiSelect"
|
||||
@close="showEmojiPicker = false"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 输入工具栏 -->
|
||||
<view class="input-toolbar">
|
||||
<!-- 左侧切换按钮 -->
|
||||
<view class="left-switch">
|
||||
<view
|
||||
class="switch-btn"
|
||||
@click="switchInputMode"
|
||||
:class="{ active: inputMode === 'voice' }"
|
||||
>
|
||||
<i
|
||||
:class="
|
||||
inputMode === 'voice' ? 'fas fa-keyboard' : 'fas fa-microphone'
|
||||
"
|
||||
></i>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 中间输入区域 -->
|
||||
<view class="input-wrapper">
|
||||
<!-- 语音模式:按住说话按钮 -->
|
||||
<view
|
||||
v-if="inputMode === 'voice'"
|
||||
class="voice-input"
|
||||
:class="{ recording: isRecording }"
|
||||
@touchstart="startVoiceRecord"
|
||||
@touchend="endVoiceRecord"
|
||||
@touchcancel="cancelVoiceRecord"
|
||||
>
|
||||
<text class="voice-text">{{
|
||||
isRecording ? "松开结束" : "按住说话"
|
||||
}}</text>
|
||||
<view v-if="isRecording" class="recording-indicator">
|
||||
<view class="recording-dot"></view>
|
||||
<view class="recording-dot"></view>
|
||||
<view class="recording-dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 文本模式:输入框 -->
|
||||
<view v-else class="text-input-container">
|
||||
<textarea
|
||||
v-model="inputMessage"
|
||||
placeholder="输入消息..."
|
||||
class="message-input"
|
||||
:style="{ height: inputHeight + 'rpx' }"
|
||||
:maxlength="500"
|
||||
@confirm="sendMessage"
|
||||
@focus="onInputFocus"
|
||||
@blur="onInputBlur"
|
||||
@input="onInputChange"
|
||||
@linechange="onLineChange"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 右侧操作区 -->
|
||||
<view class="right-actions">
|
||||
<!-- 文本模式:表情按钮 -->
|
||||
<view
|
||||
v-if="inputMode === 'text'"
|
||||
class="emoji-btn"
|
||||
@click="toggleEmojiPicker"
|
||||
:class="{ active: showEmojiPicker }"
|
||||
>
|
||||
<i class="fas fa-smile"></i>
|
||||
</view>
|
||||
|
||||
<!-- 文本模式:发送按钮(有内容时显示) -->
|
||||
<view
|
||||
v-if="inputMode === 'text' && inputMessage.trim()"
|
||||
class="send-btn"
|
||||
@click="sendMessage"
|
||||
>
|
||||
发送
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EmojiPicker from "../../src/components/EmojiPicker.vue";
|
||||
import { parseEmoji } from "../../src/utils/emojiParser.js";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EmojiPicker,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
messages: [
|
||||
{
|
||||
type: "received",
|
||||
content: "欢迎使用聊天功能!有什么可以帮您?",
|
||||
time: new Date(Date.now() - 3000 * 60 * 1000), // 30分钟前
|
||||
},
|
||||
{
|
||||
type: "received",
|
||||
content: "您可以咨询产品信息、下单流程等相关问题。",
|
||||
time: new Date(Date.now() - 29 * 60 * 1000), // 29分钟前
|
||||
},
|
||||
{
|
||||
type: "sent",
|
||||
content: "请问你们有哪些热门产品?",
|
||||
time: new Date(Date.now() - 28 * 60 * 1000), // 28分钟前
|
||||
},
|
||||
{
|
||||
type: "received",
|
||||
content: "我们的热门产品有A、B、C三款,您想了解哪一款?",
|
||||
time: new Date(Date.now() - 27 * 60 * 1000), // 27分钟前
|
||||
},
|
||||
{
|
||||
type: "received",
|
||||
content: "点击发送按钮或按回车键发送消息哦~",
|
||||
time: new Date(Date.now() - 26 * 60 * 1000), // 26分钟前
|
||||
},
|
||||
],
|
||||
inputMessage: "",
|
||||
showEmojiPicker: false,
|
||||
inputMode: "text", // 'text' 或 'voice'
|
||||
isRecording: false,
|
||||
recordingTimer: null,
|
||||
inputHeight: 80, // 输入框高度,默认1行
|
||||
scrollTop: 0, // 消息列表滚动位置
|
||||
scrollIntoView: "", // 滚动到指定元素
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
// 确保输入框初始高度为1行
|
||||
this.inputHeight = 80;
|
||||
},
|
||||
computed: {
|
||||
// 从全局数据获取设备信息
|
||||
isMobile() {
|
||||
return getApp().globalData.isMobile;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 监听消息数组变化,自动滚动到最新消息
|
||||
messages: {
|
||||
handler(newMessages, oldMessages) {
|
||||
if (newMessages.length > (oldMessages ? oldMessages.length : 0)) {
|
||||
// 有新消息时,延迟滚动确保DOM更新完成
|
||||
this.$nextTick(() => {
|
||||
setTimeout(() => {
|
||||
this.scrollToBottom();
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
immediate: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
parseEmoji(text) {
|
||||
// 使用工具函数解析emoji
|
||||
return parseEmoji(text);
|
||||
},
|
||||
sendMessage() {
|
||||
if (this.inputMessage.trim() !== "") {
|
||||
// 添加用户发送的消息
|
||||
const newMessage = {
|
||||
type: "sent",
|
||||
content: this.inputMessage,
|
||||
time: new Date(),
|
||||
};
|
||||
this.messages.push(newMessage);
|
||||
|
||||
// 强制更新视图
|
||||
this.$forceUpdate();
|
||||
|
||||
// 清空输入框并重置高度
|
||||
const message = this.inputMessage;
|
||||
this.inputMessage = "";
|
||||
this.inputHeight = 80; // 重置为1行高度
|
||||
this.showEmojiPicker = false;
|
||||
|
||||
// 立即滚动到最新消息
|
||||
this.$nextTick(() => {
|
||||
this.scrollToBottom();
|
||||
});
|
||||
|
||||
// 模拟回复
|
||||
setTimeout(() => {
|
||||
this.simulateReply(message);
|
||||
}, 1000);
|
||||
}
|
||||
},
|
||||
simulateReply(message) {
|
||||
// 简单的回复逻辑
|
||||
let reply = "感谢你的消息!";
|
||||
if (message.includes("你好")) {
|
||||
reply = "你好!很高兴见到你!";
|
||||
} else if (message.includes("产品")) {
|
||||
reply = "我们的产品非常棒,你可以查看我们的官网了解更多信息。";
|
||||
}
|
||||
|
||||
const replyMessage = {
|
||||
type: "received",
|
||||
content: reply,
|
||||
time: new Date(),
|
||||
};
|
||||
|
||||
this.messages.push(replyMessage);
|
||||
|
||||
// 强制更新视图
|
||||
this.$forceUpdate();
|
||||
|
||||
// 立即滚动到最新消息
|
||||
this.$nextTick(() => {
|
||||
this.scrollToBottom();
|
||||
});
|
||||
},
|
||||
toggleEmojiPicker() {
|
||||
this.showEmojiPicker = !this.showEmojiPicker;
|
||||
},
|
||||
onEmojiSelect(emoji) {
|
||||
this.inputMessage += emoji.unicode;
|
||||
},
|
||||
onInputFocus() {
|
||||
this.showEmojiPicker = false;
|
||||
},
|
||||
onInputBlur() {
|
||||
// 输入框失焦时的处理
|
||||
},
|
||||
onInputChange(e) {
|
||||
// 输入内容变化时的处理
|
||||
this.inputMessage = e.detail.value;
|
||||
|
||||
// 根据内容长度估算行数并调整高度
|
||||
this.adjustInputHeight();
|
||||
},
|
||||
onLineChange(e) {
|
||||
// 行数变化时调整高度,限制最大10行
|
||||
const lineCount = e.detail.lineCount;
|
||||
const minHeight = 80; // 1行高度
|
||||
const lineHeight = 40; // 每行高度
|
||||
const maxLines = 10; // 最大10行
|
||||
|
||||
let newHeight = minHeight + (lineCount - 1) * lineHeight;
|
||||
newHeight = Math.min(
|
||||
Math.max(newHeight, minHeight),
|
||||
minHeight + (maxLines - 1) * lineHeight
|
||||
);
|
||||
|
||||
this.inputHeight = newHeight;
|
||||
},
|
||||
adjustInputHeight() {
|
||||
// 根据输入内容调整高度
|
||||
const content = this.inputMessage;
|
||||
if (!content.trim()) {
|
||||
// 如果内容为空,设置为1行高度
|
||||
this.inputHeight = 80;
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = content.split("\n").length;
|
||||
const minHeight = 80;
|
||||
const lineHeight = 40;
|
||||
const maxLines = 10;
|
||||
|
||||
let newHeight = minHeight + (lines - 1) * lineHeight;
|
||||
newHeight = Math.min(
|
||||
Math.max(newHeight, minHeight),
|
||||
minHeight + (maxLines - 1) * lineHeight
|
||||
);
|
||||
|
||||
this.inputHeight = newHeight;
|
||||
},
|
||||
// 切换输入模式
|
||||
switchInputMode() {
|
||||
if (this.inputMode === "text") {
|
||||
this.inputMode = "voice";
|
||||
this.showEmojiPicker = false;
|
||||
} else {
|
||||
this.inputMode = "text";
|
||||
}
|
||||
},
|
||||
// 切换更多选项
|
||||
toggleMoreOptions() {
|
||||
// 这里可以添加更多选项的弹窗
|
||||
},
|
||||
// 语音录制相关
|
||||
startVoiceRecord() {
|
||||
this.isRecording = true;
|
||||
// 这里可以添加实际的录音逻辑
|
||||
// 例如调用 uni.getRecorderManager()
|
||||
},
|
||||
endVoiceRecord() {
|
||||
this.isRecording = false;
|
||||
// 这里可以添加录音结束的处理逻辑
|
||||
// 例如发送语音消息
|
||||
},
|
||||
cancelVoiceRecord() {
|
||||
this.isRecording = false;
|
||||
// 这里可以添加取消录音的处理逻辑
|
||||
},
|
||||
scrollToBottom() {
|
||||
// 滚动到消息列表底部
|
||||
// 方法1:使用scroll-top
|
||||
this.scrollTop = 99999;
|
||||
|
||||
// 方法2:使用scroll-into-view滚动到最后一个消息
|
||||
if (this.messages.length > 0) {
|
||||
const lastIndex = this.messages.length - 1;
|
||||
this.scrollIntoView = "message-" + lastIndex;
|
||||
|
||||
// 重置scrollIntoView以允许重复滚动
|
||||
setTimeout(() => {
|
||||
this.scrollIntoView = "";
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 返回
|
||||
*/
|
||||
goBack() {
|
||||
uni.navigateBack();
|
||||
},
|
||||
/**
|
||||
* 跳转到聊天详情
|
||||
*/
|
||||
goToChatDetail() {
|
||||
uni.navigateTo({
|
||||
url: "/pages/message/chatdetail",
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 跳转到用户详情
|
||||
*/
|
||||
goToUserDetail() {
|
||||
uni.navigateTo({
|
||||
url: "/pages/message/userdetail",
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 格式化时间
|
||||
*/
|
||||
formatTime(time) {
|
||||
const date = new Date(time);
|
||||
const now = new Date();
|
||||
|
||||
// 如果是今天,只显示时间
|
||||
if (date.toDateString() === now.toDateString()) {
|
||||
return date.toLocaleTimeString("zh-CN", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
// 如果是昨天,显示"昨天"
|
||||
const yesterday = new Date(now);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
if (date.toDateString() === yesterday.toDateString()) {
|
||||
return (
|
||||
"昨天 " +
|
||||
date.toLocaleTimeString("zh-CN", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// 其他情况显示完整日期
|
||||
return (
|
||||
date.toLocaleDateString("zh-CN") +
|
||||
" " +
|
||||
date.toLocaleTimeString("zh-CN", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chat-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
/* 移动设备顶部状态栏 */
|
||||
.top_bar {
|
||||
background: var(--gradient-primary);
|
||||
box-shadow: var(--shadow-lg);
|
||||
z-index: 9999;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: calc(var(--status-bar-height) + 88rpx);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding-top: var(--status-bar-height);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 支持安全区域的设备 */
|
||||
@supports (padding: max(0px)) {
|
||||
.top_bar {
|
||||
height: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
|
||||
padding-top: calc(var(--status-bar-height) + env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
.top_bar + .message-list {
|
||||
margin-top: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
|
||||
}
|
||||
}
|
||||
|
||||
/* 顶部导航栏 */
|
||||
.chat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 88rpx;
|
||||
background-color: var(--surface);
|
||||
border-bottom: 1rpx solid var(--border);
|
||||
padding: 0 20rpx;
|
||||
box-sizing: border-box;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
/* 移动设备下的导航栏样式 */
|
||||
.top_bar .chat-header {
|
||||
background: transparent;
|
||||
border-bottom: none;
|
||||
box-shadow: none;
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
padding: 0 20rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.top_bar .header-title {
|
||||
color: var(--white);
|
||||
font-weight: 600;
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.top_bar .header-left i,
|
||||
.top_bar .header-right i {
|
||||
color: var(--white);
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.header-left,
|
||||
.header-right {
|
||||
width: 80rpx;
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.header-right:active {
|
||||
background-color: var(--surface-hover);
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
/* 移动设备下的按钮悬停效果 */
|
||||
.top_bar .header-left:active,
|
||||
.top_bar .header-right:active {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--title-color);
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family: "iconfont" !important;
|
||||
font-size: 36rpx;
|
||||
font-style: normal;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 消息列表 */
|
||||
.message-list {
|
||||
flex: 1;
|
||||
padding: 20rpx;
|
||||
overflow-y: auto;
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
/* 移动设备下为消息列表添加顶部间距 */
|
||||
.top_bar + .message-list {
|
||||
margin-top: calc(var(--status-bar-height) + 88rpx);
|
||||
}
|
||||
|
||||
.message-item {
|
||||
display: flex;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.message-item.sent {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
/* 头像 */
|
||||
.avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 10rpx;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
margin: 0 20rpx;
|
||||
}
|
||||
|
||||
.avatar image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 消息内容 */
|
||||
.message-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 70%;
|
||||
}
|
||||
|
||||
.message-item.sent .message-content {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.message-item.received .message-content {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
/* 消息气泡 */
|
||||
.message-bubble {
|
||||
position: relative;
|
||||
padding: 20rpx;
|
||||
border-radius: 12rpx;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.4;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.message-item.sent .message-bubble {
|
||||
background: var(--gradient-primary);
|
||||
color: var(--white);
|
||||
border-radius: 12rpx 2rpx 12rpx 12rpx;
|
||||
}
|
||||
|
||||
.message-item.received .message-bubble {
|
||||
background-color: var(--surface);
|
||||
color: var(--text-color);
|
||||
border-radius: 2rpx 12rpx 12rpx 12rpx;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
/* 消息时间 */
|
||||
.message-time {
|
||||
font-size: 20rpx;
|
||||
color: var(--text-muted);
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
/* 输入区域 */
|
||||
.input-area {
|
||||
background-color: var(--surface);
|
||||
border-top: 1rpx solid var(--border);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
/* 表情选择器容器 */
|
||||
.emoji-picker-container {
|
||||
border-bottom: 1rpx solid var(--border-light);
|
||||
}
|
||||
|
||||
/* 输入工具栏 */
|
||||
.input-toolbar {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding: 20rpx 16rpx;
|
||||
gap: 16rpx;
|
||||
min-height: 100rpx;
|
||||
}
|
||||
|
||||
/* 左侧切换按钮 */
|
||||
.left-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.switch-btn {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background-color: transparent;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.switch-btn.active {
|
||||
background: var(--gradient-primary);
|
||||
}
|
||||
|
||||
.switch-btn i {
|
||||
font-size: 40rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.switch-btn.active i {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
/* 中间输入区域 */
|
||||
.input-wrapper {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 文本输入容器 */
|
||||
.text-input-container {
|
||||
background-color: var(--surface);
|
||||
border-radius: 8rpx;
|
||||
border: 1rpx solid var(--border);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.message-input {
|
||||
width: 100%;
|
||||
height: 80rpx; /* 固定默认高度为1行 */
|
||||
min-height: 80rpx;
|
||||
max-height: 440rpx; /* 10行高度:80rpx + 9 * 40rpx = 440rpx */
|
||||
padding: 20rpx 24rpx;
|
||||
border: none;
|
||||
font-size: 32rpx;
|
||||
line-height: 1.4;
|
||||
background-color: transparent;
|
||||
box-sizing: border-box;
|
||||
resize: none;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
overflow-y: auto; /* 超出10行时显示滚动条 */
|
||||
}
|
||||
|
||||
.message-input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* 语音输入 */
|
||||
.voice-input {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--gray-lighter);
|
||||
border-radius: 8rpx;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
border: 1rpx solid var(--border);
|
||||
}
|
||||
|
||||
.voice-input:active,
|
||||
.voice-input.recording {
|
||||
background-color: var(--gray);
|
||||
}
|
||||
|
||||
.voice-text {
|
||||
font-size: 32rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.recording-indicator {
|
||||
position: absolute;
|
||||
right: 20rpx;
|
||||
display: flex;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.recording-dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
background-color: #ff4444;
|
||||
border-radius: 50%;
|
||||
animation: recordingPulse 1s infinite;
|
||||
}
|
||||
|
||||
.recording-dot:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.recording-dot:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
@keyframes recordingPulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
/* 右侧操作区 */
|
||||
.right-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.emoji-btn {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background-color: transparent;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.emoji-btn.active {
|
||||
background: var(--gradient-primary);
|
||||
}
|
||||
|
||||
.emoji-btn:active {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
.emoji-btn i {
|
||||
font-size: 40rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.emoji-btn.active i {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
padding: 16rpx 32rpx;
|
||||
background: var(--gradient-primary);
|
||||
color: var(--white);
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 120rpx;
|
||||
text-align: center;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.send-btn:active {
|
||||
background: var(--primary-dark);
|
||||
transform: scale(0.98);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,475 @@
|
||||
<template>
|
||||
<view class="chat-detail-container">
|
||||
<!-- 移动设备顶部状态栏 -->
|
||||
<view class="top_bar flex w-full" v-if="isMobile">
|
||||
<view class="chat-header">
|
||||
<view class="header-left" @click="goBack">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
</view>
|
||||
<view class="header-title">聊天详情</view>
|
||||
<view class="header-right"></view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 浏览器环境顶部导航栏 -->
|
||||
<view class="chat-header" v-else>
|
||||
<view class="header-left" @click="goBack">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
</view>
|
||||
<view class="header-title">聊天详情</view>
|
||||
<view class="header-right"></view>
|
||||
</view>
|
||||
<!-- 顶部用户信息卡片 -->
|
||||
<view class="user-card">
|
||||
<view class="user-info">
|
||||
<view class="avatar-container">
|
||||
<image
|
||||
class="avatar"
|
||||
src="/static/imgs/default_avatar.png"
|
||||
mode="aspectFill"
|
||||
@error="onAvatarError"
|
||||
/>
|
||||
</view>
|
||||
<view class="user-base-info">
|
||||
<view class="nickname">{{ user.nickname }}</view>
|
||||
<view class="meteid">账号:{{ user.meteid }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 功能操作卡片 -->
|
||||
<view class="action-card">
|
||||
<view class="card-header">
|
||||
<i class="fas fa-search"></i>
|
||||
<text>聊天功能</text>
|
||||
</view>
|
||||
<view class="action-list">
|
||||
<view class="action-item" @click="goToSearchRecord">
|
||||
<view class="action-icon">
|
||||
<i class="fas fa-search"></i>
|
||||
</view>
|
||||
<text>查找聊天记录</text>
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 聊天设置卡片 -->
|
||||
<view class="setting-card">
|
||||
<view class="card-header">
|
||||
<i class="fas fa-cog"></i>
|
||||
<text>聊天设置</text>
|
||||
</view>
|
||||
<view class="setting-list">
|
||||
<view class="setting-item">
|
||||
<view class="setting-icon">
|
||||
<i class="fas fa-bell-slash"></i>
|
||||
</view>
|
||||
<text>消息免打扰</text>
|
||||
<switch :checked="disturb" @change="switchDisturb" />
|
||||
</view>
|
||||
<view class="setting-item">
|
||||
<view class="setting-icon">
|
||||
<i class="fas fa-thumbtack"></i>
|
||||
</view>
|
||||
<text>置顶聊天</text>
|
||||
<switch :checked="pinned" @change="switchPinned" />
|
||||
</view>
|
||||
<view class="setting-item" @click="chooseChatBg">
|
||||
<view class="setting-icon">
|
||||
<i class="fas fa-image"></i>
|
||||
</view>
|
||||
<text>设置当前聊天背景</text>
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 危险操作卡片 -->
|
||||
<view class="danger-card">
|
||||
<view class="card-header">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<text>危险操作</text>
|
||||
</view>
|
||||
<view class="danger-list">
|
||||
<view class="danger-item" @click="clearRecord">
|
||||
<view class="danger-icon">
|
||||
<i class="fas fa-trash"></i>
|
||||
</view>
|
||||
<text>清空聊天记录</text>
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</view>
|
||||
<view class="danger-item" @click="complain">
|
||||
<view class="danger-icon">
|
||||
<i class="fas fa-flag"></i>
|
||||
</view>
|
||||
<text>投诉</text>
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
user: {
|
||||
avatar: "",
|
||||
nickname: "美天科技用户",
|
||||
meteid: "mete_123456",
|
||||
},
|
||||
disturb: false,
|
||||
pinned: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 从全局数据获取设备信息
|
||||
isMobile() {
|
||||
return getApp().globalData.isMobile;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goBack() {
|
||||
uni.navigateBack();
|
||||
},
|
||||
onAvatarError() {
|
||||
// 头像加载失败时的处理
|
||||
console.log('头像加载失败');
|
||||
},
|
||||
goToSearchRecord() {
|
||||
uni.navigateTo({
|
||||
url: "/pages/message/chatsearch?userid=" + this.user.meteid,
|
||||
});
|
||||
},
|
||||
switchDisturb(e) {
|
||||
this.disturb = e.detail.value;
|
||||
uni.showToast({
|
||||
title: this.disturb ? "已开启免打扰" : "已关闭免打扰",
|
||||
icon: "none",
|
||||
});
|
||||
},
|
||||
switchPinned(e) {
|
||||
this.pinned = e.detail.value;
|
||||
uni.showToast({
|
||||
title: this.pinned ? "已置顶聊天" : "已取消置顶",
|
||||
icon: "none",
|
||||
});
|
||||
},
|
||||
remindOnce() {
|
||||
uni.showToast({
|
||||
title: "已为你开启消息提醒一次",
|
||||
icon: "none",
|
||||
});
|
||||
},
|
||||
chooseChatBg() {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
success: (res) => {
|
||||
const bgPath = res.tempFilePaths[0];
|
||||
uni.setStorageSync("chat_bg_" + this.user.meteid, bgPath);
|
||||
uni.showToast({ title: "聊天背景已设置", icon: "none" });
|
||||
},
|
||||
});
|
||||
},
|
||||
clearRecord() {
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: "确定要清空与该用户的聊天记录吗?",
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 这里可以调用API或清空本地数据
|
||||
uni.showToast({ title: "聊天记录已清空", icon: "none" });
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
complain() {
|
||||
uni.showModal({
|
||||
title: "投诉",
|
||||
content: "如发现对方有违规行为,请进行投诉,我们将协助核实处理。",
|
||||
confirmText: "投诉",
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showToast({
|
||||
title: "已投诉,平台将进行核查",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chat-detail-container {
|
||||
background: var(--background);
|
||||
min-height: 100vh;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
/* 移动设备顶部状态栏 */
|
||||
.top_bar {
|
||||
background: var(--gradient-primary);
|
||||
box-shadow: var(--shadow-lg);
|
||||
z-index: 9999;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: calc(var(--status-bar-height) + 88rpx);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding-top: var(--status-bar-height);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 支持安全区域的设备 */
|
||||
@supports (padding: max(0px)) {
|
||||
.top_bar {
|
||||
height: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
|
||||
padding-top: calc(var(--status-bar-height) + env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
.top_bar + .user-card {
|
||||
margin-top: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
.chat-detail-container .chat-header:not(.top_bar .chat-header) {
|
||||
height: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
|
||||
padding-top: calc(var(--status-bar-height) + env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
.chat-detail-container .chat-header:not(.top_bar .chat-header) + .user-card {
|
||||
margin-top: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
|
||||
}
|
||||
}
|
||||
|
||||
/* 顶部导航栏 */
|
||||
.chat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 88rpx;
|
||||
background-color: var(--surface);
|
||||
border-bottom: 1rpx solid var(--border);
|
||||
padding: 0 20rpx;
|
||||
box-sizing: border-box;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
/* 浏览器环境下的固定定位 */
|
||||
.chat-detail-container .chat-header:not(.top_bar .chat-header) {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 9999;
|
||||
height: calc(var(--status-bar-height) + 88rpx);
|
||||
padding-top: var(--status-bar-height);
|
||||
}
|
||||
|
||||
/* 移动设备下的导航栏样式 */
|
||||
.top_bar .chat-header {
|
||||
background: transparent;
|
||||
border-bottom: none;
|
||||
box-shadow: none;
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
padding: 0 20rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.top_bar .header-title {
|
||||
color: var(--white);
|
||||
font-weight: 600;
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.top_bar .header-left i,
|
||||
.top_bar .header-right i {
|
||||
color: var(--white);
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.header-left,
|
||||
.header-right {
|
||||
width: 80rpx;
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.header-left:active,
|
||||
.header-right:active {
|
||||
background-color: var(--gray-lighter);
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.top_bar .header-left:active,
|
||||
.top_bar .header-right:active {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--title-color);
|
||||
}
|
||||
|
||||
/* 移动设备下为内容添加顶部间距 */
|
||||
.top_bar + .user-card {
|
||||
margin-top: calc(var(--status-bar-height) + 88rpx);
|
||||
}
|
||||
|
||||
/* 浏览器环境下为内容添加顶部间距 */
|
||||
.chat-detail-container .chat-header:not(.top_bar .chat-header) + .user-card {
|
||||
margin-top: calc(var(--status-bar-height) + 88rpx);
|
||||
}
|
||||
|
||||
/* 用户信息卡片 */
|
||||
.user-card {
|
||||
background: var(--surface);
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 30rpx;
|
||||
box-shadow: var(--shadow-md);
|
||||
border: 1rpx solid var(--border-light);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 40rpx 30rpx;
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
margin-right: 30rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 16rpx;
|
||||
background: var(--gray);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.user-base-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nickname {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: var(--title-color);
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.meteid {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 卡片通用样式 */
|
||||
.action-card, .setting-card, .danger-card {
|
||||
background: var(--surface);
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: var(--shadow);
|
||||
border: 1rpx solid var(--border-light);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx 30rpx 20rpx 30rpx;
|
||||
border-bottom: 1rpx solid var(--border-light);
|
||||
background: var(--gray-lighter);
|
||||
}
|
||||
|
||||
.card-header i {
|
||||
font-size: 28rpx;
|
||||
color: var(--icon-color);
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.card-header text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: var(--title-color);
|
||||
}
|
||||
|
||||
/* 操作列表 */
|
||||
.action-list, .setting-list, .danger-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.action-item, .setting-item, .danger-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid var(--border-light);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.action-item:last-child, .setting-item:last-child, .danger-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.action-item:active, .setting-item:active, .danger-item:active {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
/* 图标样式 */
|
||||
.action-icon, .setting-icon, .danger-icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
background: var(--gradient-primary);
|
||||
}
|
||||
|
||||
.setting-icon {
|
||||
background: var(--gradient-primary);
|
||||
}
|
||||
|
||||
.danger-icon {
|
||||
background: var(--gradient-error);
|
||||
}
|
||||
|
||||
.action-icon i, .setting-icon i, .danger-icon i {
|
||||
color: var(--white);
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
/* 文字样式 */
|
||||
.action-item text, .setting-item text, .danger-item text {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.danger-item text {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.action-item i.fas.fa-chevron-right, .setting-item i.fas.fa-chevron-right, .danger-item i.fas.fa-chevron-right {
|
||||
color: var(--text-muted);
|
||||
font-size: 24rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,589 @@
|
||||
<template>
|
||||
<view class="user-detail-container">
|
||||
<!-- 移动设备顶部状态栏 -->
|
||||
<view class="top_bar flex w-full" v-if="isMobile">
|
||||
<view class="chat-header">
|
||||
<view class="header-left" @click="goBack">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
</view>
|
||||
<view class="header-title">用户详情</view>
|
||||
<view class="header-right"></view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 浏览器环境顶部导航栏 -->
|
||||
<view class="chat-header" v-else>
|
||||
<view class="header-left" @click="goBack">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
</view>
|
||||
<view class="header-title">用户详情</view>
|
||||
<view class="header-right"></view>
|
||||
</view>
|
||||
<!-- 顶部用户信息卡片 -->
|
||||
<view class="user-card">
|
||||
<view class="user-info">
|
||||
<view class="avatar-container">
|
||||
<image
|
||||
class="avatar"
|
||||
:src="user.avatar || '/static/imgs/default_avatar.png'"
|
||||
mode="aspectFill"
|
||||
@error="onAvatarError"
|
||||
/>
|
||||
<view class="online-status" v-if="user.isOnline"></view>
|
||||
</view>
|
||||
<view class="user-base-info">
|
||||
<view class="nickname">{{ user.nickname }}</view>
|
||||
<view class="meteid">账号:{{ user.meteid }}</view>
|
||||
<view class="meta-info">
|
||||
<view class="meta-item" v-if="user.department">
|
||||
部门:<text>{{ user.department }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="meta-info">
|
||||
<view class="meta-item" v-if="user.position">
|
||||
职位:<text>{{ user.position }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 功能按钮区域 -->
|
||||
<view class="action-section">
|
||||
<view class="action-btn primary" @click="sendMessage">
|
||||
<i class="fas fa-comment-dots"></i>
|
||||
<text>发消息</text>
|
||||
</view>
|
||||
<view class="action-btn" @click="startAudioCall">
|
||||
<i class="fas fa-phone-alt"></i>
|
||||
<text>语音通话</text>
|
||||
</view>
|
||||
<view class="action-btn" @click="startVideoCall">
|
||||
<i class="fas fa-video"></i>
|
||||
<text>视频通话</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 资料信息卡片 -->
|
||||
<view class="info-card">
|
||||
<view class="card-header">
|
||||
<i class="fas fa-user-circle"></i>
|
||||
<text>个人资料</text>
|
||||
</view>
|
||||
<view class="info-list">
|
||||
<view class="info-item" v-if="user.phone" @click="copyToClipboard(user.phone, '手机号')">
|
||||
<view class="info-icon phone">
|
||||
<i class="fas fa-phone"></i>
|
||||
</view>
|
||||
<view class="info-content">
|
||||
<text class="info-label">手机号码</text>
|
||||
<text class="info-value">{{ user.phone }}</text>
|
||||
</view>
|
||||
<i class="fas fa-copy"></i>
|
||||
</view>
|
||||
<view class="info-item" v-if="user.wechat" @click="copyToClipboard(user.wechat, '微信号')">
|
||||
<view class="info-icon wechat">
|
||||
<i class="fab fa-weixin"></i>
|
||||
</view>
|
||||
<view class="info-content">
|
||||
<text class="info-label">微信</text>
|
||||
<text class="info-value">{{ user.wechat }}</text>
|
||||
</view>
|
||||
<i class="fas fa-copy"></i>
|
||||
</view>
|
||||
<view class="info-item" v-if="user.email" @click="copyToClipboard(user.email, '邮箱')">
|
||||
<view class="info-icon email">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</view>
|
||||
<view class="info-content">
|
||||
<text class="info-label">邮箱</text>
|
||||
<text class="info-value">{{ user.email }}</text>
|
||||
</view>
|
||||
<i class="fas fa-copy"></i>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 更多功能卡片 -->
|
||||
<view class="more-card">
|
||||
<view class="card-header">
|
||||
<i class="fas fa-cog"></i>
|
||||
<text>更多功能</text>
|
||||
</view>
|
||||
<view class="more-list">
|
||||
<view class="more-item" @click="addToContacts">
|
||||
<view class="more-icon">
|
||||
<i class="fas fa-user-plus"></i>
|
||||
</view>
|
||||
<text>添加到通讯录</text>
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</view>
|
||||
<view class="more-item" @click="shareContact">
|
||||
<view class="more-icon">
|
||||
<i class="fas fa-share-alt"></i>
|
||||
</view>
|
||||
<text>发送名片</text>
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</view>
|
||||
<view class="more-item" @click="viewMoments">
|
||||
<view class="more-icon">
|
||||
<i class="fas fa-images"></i>
|
||||
</view>
|
||||
<text>查看朋友圈</text>
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
user: {
|
||||
avatar: '',
|
||||
nickname: '张三',
|
||||
meteid: 'mete_10001',
|
||||
position: '产品经理',
|
||||
department: '产品部',
|
||||
phone: '13455668888',
|
||||
wechat: 'zhangsan_weixin',
|
||||
email: 'zhangsan@email.com',
|
||||
isOnline: true
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 从全局数据获取设备信息
|
||||
isMobile() {
|
||||
return getApp().globalData.isMobile;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goBack() {
|
||||
uni.navigateBack();
|
||||
},
|
||||
onAvatarError(e) {
|
||||
this.user.avatar = '/static/imgs/default_avatar.png'
|
||||
},
|
||||
sendMessage() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/message/chat?meteid=' + this.user.meteid
|
||||
});
|
||||
},
|
||||
startAudioCall() {
|
||||
uni.showToast({
|
||||
title: '语音通话功能暂未开放',
|
||||
icon: 'none'
|
||||
});
|
||||
},
|
||||
startVideoCall() {
|
||||
uni.showToast({
|
||||
title: '视频通话功能暂未开放',
|
||||
icon: 'none'
|
||||
});
|
||||
},
|
||||
copyToClipboard(text, type) {
|
||||
uni.setClipboardData({
|
||||
data: text,
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: `${type}已复制`,
|
||||
icon: 'success'
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
addToContacts() {
|
||||
uni.showToast({
|
||||
title: '已添加到通讯录',
|
||||
icon: 'success'
|
||||
});
|
||||
},
|
||||
shareContact() {
|
||||
uni.showToast({
|
||||
title: '已发送名片',
|
||||
icon: 'success'
|
||||
});
|
||||
},
|
||||
viewMoments() {
|
||||
uni.showToast({
|
||||
title: '朋友圈功能暂未开放',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-detail-container {
|
||||
background: var(--background);
|
||||
min-height: 100vh;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
/* 移动设备顶部状态栏 */
|
||||
.top_bar {
|
||||
background: var(--gradient-primary);
|
||||
box-shadow: var(--shadow-lg);
|
||||
z-index: 9999;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: calc(var(--status-bar-height) + 88rpx);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding-top: var(--status-bar-height);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 支持安全区域的设备 */
|
||||
@supports (padding: max(0px)) {
|
||||
.top_bar {
|
||||
height: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
|
||||
padding-top: calc(var(--status-bar-height) + env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
.top_bar + .user-card {
|
||||
margin-top: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
.user-detail-container .chat-header:not(.top_bar .chat-header) {
|
||||
height: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
|
||||
padding-top: calc(var(--status-bar-height) + env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
.user-detail-container .chat-header:not(.top_bar .chat-header) + .user-card {
|
||||
margin-top: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
|
||||
}
|
||||
}
|
||||
|
||||
/* 顶部导航栏 */
|
||||
.chat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 88rpx;
|
||||
background-color: var(--surface);
|
||||
border-bottom: 1rpx solid var(--border);
|
||||
padding: 0 20rpx;
|
||||
box-sizing: border-box;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
/* 浏览器环境下的固定定位 */
|
||||
.user-detail-container .chat-header:not(.top_bar .chat-header) {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 9999;
|
||||
height: calc(var(--status-bar-height) + 88rpx);
|
||||
padding-top: var(--status-bar-height);
|
||||
}
|
||||
|
||||
/* 移动设备下的导航栏样式 */
|
||||
.top_bar .chat-header {
|
||||
background: transparent;
|
||||
border-bottom: none;
|
||||
box-shadow: none;
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
padding: 0 20rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.top_bar .header-title {
|
||||
color: var(--white);
|
||||
font-weight: 600;
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.top_bar .header-left i,
|
||||
.top_bar .header-right i {
|
||||
color: var(--white);
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.header-left,
|
||||
.header-right {
|
||||
width: 80rpx;
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.header-left:active,
|
||||
.header-right:active {
|
||||
background-color: var(--gray-lighter);
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.top_bar .header-left:active,
|
||||
.top_bar .header-right:active {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--title-color);
|
||||
}
|
||||
|
||||
/* 移动设备下为内容添加顶部间距 */
|
||||
.top_bar + .user-card {
|
||||
margin-top: calc(var(--status-bar-height) + 88rpx);
|
||||
}
|
||||
|
||||
/* 浏览器环境下为内容添加顶部间距 */
|
||||
.user-detail-container .chat-header:not(.top_bar .chat-header) + .user-card {
|
||||
margin-top: calc(var(--status-bar-height) + 88rpx);
|
||||
}
|
||||
|
||||
/* 用户信息卡片 */
|
||||
.user-card {
|
||||
background: var(--surface);
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 30rpx;
|
||||
box-shadow: var(--shadow-md);
|
||||
overflow: hidden;
|
||||
border: 1rpx solid var(--border-light);
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 40rpx 30rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
position: relative;
|
||||
margin-right: 30rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 16rpx;
|
||||
background: var(--gray-light);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.online-status {
|
||||
position: absolute;
|
||||
bottom: 8rpx;
|
||||
right: 8rpx;
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
background: var(--success);
|
||||
border: 4rpx solid var(--white);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.user-base-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nickname {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: var(--title-color);
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.meteid {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.meta-info {
|
||||
display: flex;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 24rpx;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.meta-item i {
|
||||
margin-right: 12rpx;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
/* 功能按钮区域 */
|
||||
.action-section {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
background: var(--surface);
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx 16rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
box-shadow: var(--shadow);
|
||||
border: 1rpx solid var(--border-light);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.action-btn:active {
|
||||
transform: translateY(2rpx);
|
||||
box-shadow: var(--shadow-md);
|
||||
background: var(--surface-hover);
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: var(--gradient-primary);
|
||||
color: var(--white);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.action-btn i {
|
||||
font-size: 32rpx;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.action-btn text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 信息卡片 */
|
||||
.info-card, .more-card {
|
||||
background: var(--surface);
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: var(--shadow);
|
||||
border: 1rpx solid var(--border-light);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx 30rpx 20rpx 30rpx;
|
||||
border-bottom: 1rpx solid var(--border-light);
|
||||
background: var(--gray-lighter);
|
||||
}
|
||||
|
||||
.card-header i {
|
||||
font-size: 28rpx;
|
||||
color: var(--icon-color);
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.card-header text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: var(--title-color);
|
||||
}
|
||||
|
||||
.info-list, .more-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.info-item, .more-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid var(--border-light);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.info-item:last-child, .more-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-item:active, .more-item:active {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.info-icon.phone {
|
||||
background: var(--gradient-success);
|
||||
}
|
||||
|
||||
.info-icon.wechat {
|
||||
background: var(--gradient-success);
|
||||
}
|
||||
|
||||
.info-icon.email {
|
||||
background: var(--gradient-warning);
|
||||
}
|
||||
|
||||
.info-icon i {
|
||||
color: var(--white);
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 28rpx;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info-item i.fas.fa-copy, .more-item i.fas.fa-chevron-right {
|
||||
color: var(--text-muted);
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.more-icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 12rpx;
|
||||
background: var(--gradient-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.more-icon i {
|
||||
color: var(--white);
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.more-item text {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,370 @@
|
||||
<template>
|
||||
<view class="splash-container">
|
||||
<!-- 背景动画 -->
|
||||
<view class="splash-background">
|
||||
<view class="gradient-circle circle-1"></view>
|
||||
<view class="gradient-circle circle-2"></view>
|
||||
<view class="gradient-circle circle-3"></view>
|
||||
</view>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<view class="splash-content">
|
||||
<!-- Logo区域 -->
|
||||
<view class="logo-section">
|
||||
<view class="logo-container">
|
||||
<text class="logo-text">{{appInfo.name}}</text>
|
||||
<view class="logo-subtitle">{{appInfo.nameEn}}</view>
|
||||
</view>
|
||||
<view class="logo-icon">
|
||||
<text class="icon-text">{{appInfo.logo}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载动画 -->
|
||||
<view class="loading-section">
|
||||
<view class="loading-dots">
|
||||
<view class="dot" :class="{ active: loadingStep >= 1 }"></view>
|
||||
<view class="dot" :class="{ active: loadingStep >= 2 }"></view>
|
||||
<view class="dot" :class="{ active: loadingStep >= 3 }"></view>
|
||||
</view>
|
||||
<text class="loading-text">{{loadingText}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 版本信息 -->
|
||||
<!-- <view class="version-info">
|
||||
<text class="version-text">v{{appVersion}}</text>
|
||||
</view> -->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useAuthStore } from '../../src/store/authStore.js'
|
||||
import {
|
||||
initSplash,
|
||||
shouldShowSplash,
|
||||
markSplashShown,
|
||||
getSplashDuration,
|
||||
loadingSteps,
|
||||
executeLoadingStep,
|
||||
onSplashComplete
|
||||
} from '../../src/utils/splashManager.js'
|
||||
import { getAppInfo, getLoadingSteps, getThemeConfig } from '../../src/config/splash.js'
|
||||
|
||||
export default {
|
||||
name: 'SplashScreen',
|
||||
setup() {
|
||||
const authStore = useAuthStore()
|
||||
const loadingStep = ref(0)
|
||||
const loadingText = ref('正在初始化...')
|
||||
const isLoading = ref(false)
|
||||
|
||||
// 获取配置信息
|
||||
const appInfo = getAppInfo()
|
||||
const themeConfig = getThemeConfig()
|
||||
const configLoadingSteps = getLoadingSteps()
|
||||
|
||||
// 初始化启动画面
|
||||
initSplash()
|
||||
|
||||
// 启动加载动画
|
||||
const startLoading = async () => {
|
||||
if (isLoading.value) return
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
// 执行每个加载步骤
|
||||
for (let i = 0; i < configLoadingSteps.length; i++) {
|
||||
const step = configLoadingSteps[i]
|
||||
|
||||
// 更新UI
|
||||
loadingStep.value = i + 1
|
||||
loadingText.value = step.text
|
||||
|
||||
// 执行步骤
|
||||
const success = await executeLoadingStep(step)
|
||||
|
||||
if (!success) {
|
||||
console.warn(`步骤 ${step.action} 执行失败,继续下一步`)
|
||||
}
|
||||
|
||||
// 等待步骤完成时间
|
||||
await new Promise(resolve => setTimeout(resolve, step.duration))
|
||||
}
|
||||
|
||||
// 确保最小显示时间
|
||||
const minDuration = getSplashDuration()
|
||||
if (minDuration > 0) {
|
||||
await new Promise(resolve => setTimeout(resolve, minDuration))
|
||||
}
|
||||
|
||||
// 启动完成
|
||||
onSplashComplete()
|
||||
|
||||
// 根据登录状态跳转
|
||||
if (authStore.isAuthenticated) {
|
||||
// 已登录,跳转到主页面
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
} else {
|
||||
// 未登录,跳转到登录页面
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/index'
|
||||
})
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('启动过程出错:', error)
|
||||
// 即使出错也要跳转,根据登录状态决定
|
||||
if (authStore.isAuthenticated) {
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
} else {
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/index'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 检查是否应该显示启动画面
|
||||
if (shouldShowSplash()) {
|
||||
// 延迟启动动画,让用户看到启动画面
|
||||
setTimeout(() => {
|
||||
startLoading()
|
||||
}, 500)
|
||||
} else {
|
||||
// 直接跳转,根据登录状态决定
|
||||
setTimeout(() => {
|
||||
if (authStore.isAuthenticated) {
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
} else {
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/index'
|
||||
})
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
loadingStep,
|
||||
loadingText,
|
||||
appVersion: appInfo.version,
|
||||
appInfo,
|
||||
isLoading
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.splash-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.splash-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gradient-circle {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.circle-1 {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
top: 10%;
|
||||
left: 10%;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.circle-2 {
|
||||
width: 300rpx;
|
||||
height: 300rpx;
|
||||
top: 60%;
|
||||
right: 10%;
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
.circle-3 {
|
||||
width: 150rpx;
|
||||
height: 150rpx;
|
||||
top: 30%;
|
||||
right: 30%;
|
||||
animation-delay: 4s;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0px) rotate(0deg);
|
||||
opacity: 0.7;
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-20px) rotate(180deg);
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
.splash-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.logo-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 120rpx;
|
||||
animation: fadeInUp 1s ease-out;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
text-align: center;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 64rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
text-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.3);
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.logo-subtitle {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(20rpx);
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.2);
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.icon-text {
|
||||
font-size: 60rpx;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 80rpx;
|
||||
animation: fadeInUp 1s ease-out 0.5s both;
|
||||
}
|
||||
|
||||
.loading-dots {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dot.active {
|
||||
background: #fff;
|
||||
transform: scale(1.2);
|
||||
box-shadow: 0 0 20rpx rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.version-info {
|
||||
position: absolute;
|
||||
bottom: 60rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
animation: fadeInUp 1s ease-out 1s both;
|
||||
}
|
||||
|
||||
.version-text {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media screen and (max-width: 750rpx) {
|
||||
.logo-text {
|
||||
font-size: 56rpx;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
}
|
||||
|
||||
.icon-text {
|
||||
font-size: 50rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,460 @@
|
||||
<template>
|
||||
<view class="main-data-page">
|
||||
<!-- 使用子头部组件 -->
|
||||
<sub-header
|
||||
title="业务审批"
|
||||
@goBack="goBack"
|
||||
@showMoreOptions="showMoreOptions"
|
||||
/>
|
||||
<view class="main-container">
|
||||
<!-- 审批状态tabs -->
|
||||
<view class="approval-tabs">
|
||||
<view
|
||||
v-for="tab in approvalTabs"
|
||||
:key="tab.key"
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@click="switchTab(tab.key)"
|
||||
>
|
||||
{{ tab.label }}
|
||||
<view v-if="(tab.key === 'pending' || tab.key === 'all') && tab.count > 0" class="tab-badge">{{ tab.count }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 审批列表 -->
|
||||
<view class="approval-list">
|
||||
<view
|
||||
v-for="item in filteredApprovalList"
|
||||
:key="item.id"
|
||||
class="approval-item"
|
||||
@click="viewDetail(item)"
|
||||
>
|
||||
<view class="item-header">
|
||||
<view class="item-title">{{ item.title }}</view>
|
||||
<view class="item-status" :class="item.status">
|
||||
{{ getStatusText(item.status) }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="item-content">
|
||||
<view class="item-info">
|
||||
<text class="info-label">申请人:</text>
|
||||
<text class="info-value">{{ item.applicant }}</text>
|
||||
</view>
|
||||
<view class="item-info">
|
||||
<text class="info-label">申请时间:</text>
|
||||
<text class="info-value">{{ item.applyTime }}</text>
|
||||
</view>
|
||||
<view class="item-info">
|
||||
<text class="info-label">业务类型:</text>
|
||||
<text class="info-value">{{ item.businessType }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="item-footer">
|
||||
<view class="priority" :class="item.priority">
|
||||
{{ getPriorityText(item.priority) }}
|
||||
</view>
|
||||
<view class="item-amount" v-if="item.amount">
|
||||
金额:{{ item.amount }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-if="filteredApprovalList.length === 0" class="empty-state">
|
||||
<view class="empty-icon">📋</view>
|
||||
<view class="empty-text">暂无{{ getCurrentTabLabel() }}审批</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import SubHeader from "../components/subHeader.vue";
|
||||
|
||||
// 响应式数据
|
||||
const activeTab = ref('all'); // 当前激活的tab
|
||||
|
||||
// 审批状态tabs配置
|
||||
const approvalTabs = ref([
|
||||
{ key: 'all', label: '全部', count: 0 },
|
||||
{ key: 'pending', label: '待审批', count: 0 },
|
||||
{ key: 'approved', label: '已通过', count: 0 },
|
||||
{ key: 'rejected', label: '已拒绝', count: 0 }
|
||||
]);
|
||||
|
||||
// 审批列表数据
|
||||
const approvalList = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: '港口作业申请',
|
||||
status: 'pending',
|
||||
applicant: '张三',
|
||||
applyTime: '2024-01-15 09:30',
|
||||
businessType: '港口作业',
|
||||
priority: 'high',
|
||||
amount: '¥50,000'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '设备维修申请',
|
||||
status: 'approved',
|
||||
applicant: '李四',
|
||||
applyTime: '2024-01-14 14:20',
|
||||
businessType: '设备维护',
|
||||
priority: 'medium',
|
||||
amount: '¥15,000'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '人员调动申请',
|
||||
status: 'rejected',
|
||||
applicant: '王五',
|
||||
applyTime: '2024-01-13 16:45',
|
||||
businessType: '人事管理',
|
||||
priority: 'low',
|
||||
amount: null
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '采购申请',
|
||||
status: 'pending',
|
||||
applicant: '赵六',
|
||||
applyTime: '2024-01-12 11:15',
|
||||
businessType: '采购管理',
|
||||
priority: 'high',
|
||||
amount: '¥80,000'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: '安全培训申请',
|
||||
status: 'approved',
|
||||
applicant: '钱七',
|
||||
applyTime: '2024-01-11 08:30',
|
||||
businessType: '培训管理',
|
||||
priority: 'medium',
|
||||
amount: '¥5,000'
|
||||
}
|
||||
]);
|
||||
|
||||
// 计算属性:根据当前tab过滤列表
|
||||
const filteredApprovalList = computed(() => {
|
||||
let filteredList = [];
|
||||
|
||||
if (activeTab.value === 'all') {
|
||||
filteredList = approvalList.value;
|
||||
} else {
|
||||
filteredList = approvalList.value.filter(item => item.status === activeTab.value);
|
||||
}
|
||||
|
||||
// 按照状态排序:待审批、已拒绝、已通过
|
||||
const statusOrder = ['pending', 'rejected', 'approved'];
|
||||
return filteredList.sort((a, b) => {
|
||||
const aIndex = statusOrder.indexOf(a.status);
|
||||
const bIndex = statusOrder.indexOf(b.status);
|
||||
return aIndex - bIndex;
|
||||
});
|
||||
});
|
||||
|
||||
// 更新tabs计数
|
||||
const updateTabCounts = () => {
|
||||
approvalTabs.value.forEach(tab => {
|
||||
if (tab.key === 'all') {
|
||||
tab.count = approvalList.value.length;
|
||||
} else {
|
||||
tab.count = approvalList.value.filter(item => item.status === tab.key).length;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 切换tab
|
||||
const switchTab = (tabKey) => {
|
||||
activeTab.value = tabKey;
|
||||
};
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
pending: '待审批',
|
||||
approved: '已通过',
|
||||
rejected: '已拒绝'
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
};
|
||||
|
||||
// 获取优先级文本
|
||||
const getPriorityText = (priority) => {
|
||||
const priorityMap = {
|
||||
high: '高',
|
||||
medium: '中',
|
||||
low: '低'
|
||||
};
|
||||
return priorityMap[priority] || priority;
|
||||
};
|
||||
|
||||
// 获取当前tab标签
|
||||
const getCurrentTabLabel = () => {
|
||||
const currentTab = approvalTabs.value.find(tab => tab.key === activeTab.value);
|
||||
return currentTab ? currentTab.label : '';
|
||||
};
|
||||
|
||||
// 查看详情
|
||||
const viewDetail = (item) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/tasks/approvalDetail?id=${item.id}&title=${encodeURIComponent(item.title)}&status=${item.status}&applicant=${encodeURIComponent(item.applicant)}&applyTime=${encodeURIComponent(item.applyTime)}&businessType=${encodeURIComponent(item.businessType)}&priority=${item.priority}&amount=${item.amount ? encodeURIComponent(item.amount) : ''}`
|
||||
});
|
||||
};
|
||||
|
||||
// 头部相关方法
|
||||
const goBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
const showMoreOptions = () => {
|
||||
uni.showActionSheet({
|
||||
itemList: ["刷新", "返回首页"],
|
||||
success: (res) => {
|
||||
switch (res.tapIndex) {
|
||||
case 0:
|
||||
// 刷新数据
|
||||
updateTabCounts();
|
||||
uni.showToast({ title: "数据已刷新", icon: "success" });
|
||||
break;
|
||||
case 1:
|
||||
uni.reLaunch({
|
||||
url: "/pages/index/index",
|
||||
});
|
||||
uni.showToast({ title: "已返回工作台", icon: "success" });
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 页面加载时处理URL参数
|
||||
onMounted(() => {
|
||||
const pages = getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
const options = currentPage.options;
|
||||
|
||||
// 如果URL中有activeTab参数,则设置为对应的tab
|
||||
if (options.activeTab) {
|
||||
activeTab.value = options.activeTab;
|
||||
}
|
||||
|
||||
// 初始化计数
|
||||
updateTabCounts();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
// 变量定义
|
||||
$border-radius: 16rpx;
|
||||
$border-radius-small: 12rpx;
|
||||
$border-radius-badge: 20rpx;
|
||||
$spacing-small: 12rpx;
|
||||
$spacing-medium: 20rpx;
|
||||
$spacing-large: 30rpx;
|
||||
$transition: all 0.3s ease;
|
||||
|
||||
.main-data-page {
|
||||
background: var(--background);
|
||||
min-height: 100vh;
|
||||
padding-top: calc(var(--status-bar-height) + 88rpx);
|
||||
padding-bottom: 40rpx;
|
||||
|
||||
.main-container {
|
||||
padding: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 审批状态tabs
|
||||
.approval-tabs {
|
||||
display: flex;
|
||||
background: var(--surface);
|
||||
border-radius: $border-radius;
|
||||
margin-bottom: $spacing-large;
|
||||
box-shadow: var(--shadow-md);
|
||||
border: 1rpx solid var(--border-light);
|
||||
overflow: hidden;
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24rpx 16rpx;
|
||||
font-size: 28rpx;
|
||||
color: var(--text-secondary);
|
||||
background: var(--surface);
|
||||
transition: $transition;
|
||||
position: relative;
|
||||
|
||||
&.active {
|
||||
background: var(--primary-color);
|
||||
color: var(--white);
|
||||
font-weight: 600;
|
||||
|
||||
.tab-badge {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.tab-badge {
|
||||
background: var(--error);
|
||||
color: var(--white);
|
||||
font-size: 20rpx;
|
||||
padding: 4rpx 8rpx;
|
||||
border-radius: $border-radius-badge;
|
||||
margin-left: 8rpx;
|
||||
min-width: 32rpx;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 审批列表
|
||||
.approval-list {
|
||||
.approval-item {
|
||||
background: var(--surface);
|
||||
border-radius: $border-radius;
|
||||
margin-bottom: $spacing-medium;
|
||||
padding: $spacing-large;
|
||||
box-shadow: var(--shadow);
|
||||
border: 1rpx solid var(--border-light);
|
||||
transition: $transition;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: var(--shadow-md);
|
||||
background: var(--surface-hover);
|
||||
}
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: $spacing-medium;
|
||||
|
||||
.item-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--title-color);
|
||||
flex: 1;
|
||||
margin-right: $spacing-medium;
|
||||
}
|
||||
|
||||
.item-status {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: $border-radius-badge;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
|
||||
&.pending {
|
||||
background: var(--warning-light);
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
&.approved {
|
||||
background: var(--success-light);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
&.rejected {
|
||||
background: var(--error-light);
|
||||
color: var(--error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-content {
|
||||
margin-bottom: $spacing-medium;
|
||||
|
||||
.item-info {
|
||||
display: flex;
|
||||
margin-bottom: $spacing-small;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 26rpx;
|
||||
color: var(--text-muted);
|
||||
width: 140rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 26rpx;
|
||||
color: var(--text-color);
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.priority {
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: $border-radius-small;
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
|
||||
&.high {
|
||||
background: var(--error-light);
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
&.medium {
|
||||
background: var(--warning-light);
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
&.low {
|
||||
background: var(--success-light);
|
||||
color: var(--success);
|
||||
}
|
||||
}
|
||||
|
||||
.item-amount {
|
||||
font-size: 26rpx;
|
||||
color: var(--orange);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 空状态
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 120rpx 60rpx;
|
||||
|
||||
.empty-icon {
|
||||
font-size: 120rpx;
|
||||
margin-bottom: $spacing-large;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
// 支持安全区域的设备
|
||||
@supports (padding: max(0px)) {
|
||||
.main-data-page {
|
||||
padding-top: calc(
|
||||
var(--status-bar-height) + 88rpx + env(safe-area-inset-top)
|
||||
);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,484 @@
|
||||
<template>
|
||||
<view class="main-data-page">
|
||||
<!-- 使用子头部组件 -->
|
||||
<sub-header
|
||||
title="审批详情"
|
||||
@goBack="goBack"
|
||||
@showMoreOptions="showMoreOptions"
|
||||
/>
|
||||
<view class="main-container">
|
||||
<!-- 审批详情卡片 -->
|
||||
<view class="detail-card">
|
||||
<!-- 标题和状态 -->
|
||||
<view class="detail-header">
|
||||
<view class="detail-title">{{ approvalDetail.title }}</view>
|
||||
<view class="detail-status" :class="approvalDetail.status">
|
||||
{{ getStatusText(approvalDetail.status) }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<view class="detail-section">
|
||||
<view class="section-title">基本信息</view>
|
||||
<view class="info-list">
|
||||
<view class="info-item">
|
||||
<text class="info-label">申请人:</text>
|
||||
<text class="info-value">{{ approvalDetail.applicant }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">申请时间:</text>
|
||||
<text class="info-value">{{ approvalDetail.applyTime }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">业务类型:</text>
|
||||
<text class="info-value">{{ approvalDetail.businessType }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">优先级:</text>
|
||||
<view class="priority-tag" :class="approvalDetail.priority">
|
||||
{{ getPriorityText(approvalDetail.priority) }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="info-item" v-if="approvalDetail.amount">
|
||||
<text class="info-label">申请金额:</text>
|
||||
<text class="info-value amount">{{ approvalDetail.amount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 审批信息 -->
|
||||
<view class="detail-section">
|
||||
<view class="section-title">审批信息</view>
|
||||
<view class="info-list">
|
||||
<view class="info-item">
|
||||
<text class="info-label">审批人:</text>
|
||||
<text class="info-value">{{ approvalDetail.approver || '待分配' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="info-label">审批时间:</text>
|
||||
<text class="info-value">{{ approvalDetail.approveTime || '待审批' }}</text>
|
||||
</view>
|
||||
<view class="info-item" v-if="approvalDetail.remark">
|
||||
<text class="info-label">审批意见:</text>
|
||||
<text class="info-value">{{ approvalDetail.remark }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="action-buttons" v-if="approvalDetail.status === 'pending'">
|
||||
<button class="action-btn reject-btn" @click="handleApproval('rejected')">
|
||||
拒绝
|
||||
</button>
|
||||
<button class="action-btn approve-btn" @click="handleApproval('approved')">
|
||||
通过
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 理由输入弹窗 -->
|
||||
<view v-if="showReasonDialog" class="reason-mask" @click.self="closeReasonDialog">
|
||||
<view class="reason-modal">
|
||||
<view class="reason-title">{{ actionType === 'approved' ? '通过理由' : '拒绝理由' }}</view>
|
||||
<textarea class="reason-textarea" v-model="reasonText" placeholder="请输入审批理由(必填)" auto-height :maxlength="200" />
|
||||
<view class="reason-tip">最多200字,需填写后才能提交</view>
|
||||
<view class="reason-actions">
|
||||
<button class="reason-btn cancel" @click="closeReasonDialog">取消</button>
|
||||
<button class="reason-btn confirm" @click="submitApproval">提交</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import SubHeader from "../components/subHeader.vue";
|
||||
|
||||
// 审批详情数据
|
||||
const approvalDetail = ref({
|
||||
id: '',
|
||||
title: '',
|
||||
status: '',
|
||||
applicant: '',
|
||||
applyTime: '',
|
||||
businessType: '',
|
||||
priority: '',
|
||||
amount: '',
|
||||
approver: '',
|
||||
approveTime: '',
|
||||
remark: ''
|
||||
});
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
pending: '待审批',
|
||||
approved: '已通过',
|
||||
rejected: '已拒绝'
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
};
|
||||
|
||||
// 获取优先级文本
|
||||
const getPriorityText = (priority) => {
|
||||
const priorityMap = {
|
||||
high: '高',
|
||||
medium: '中',
|
||||
low: '低'
|
||||
};
|
||||
return priorityMap[priority] || priority;
|
||||
};
|
||||
|
||||
// 理由弹框状态
|
||||
const showReasonDialog = ref(false);
|
||||
const actionType = ref('approved'); // 'approved' | 'rejected'
|
||||
const reasonText = ref('');
|
||||
|
||||
// 处理审批操作(先弹出理由框)
|
||||
const handleApproval = (action) => {
|
||||
actionType.value = action;
|
||||
reasonText.value = '';
|
||||
showReasonDialog.value = true;
|
||||
};
|
||||
|
||||
const closeReasonDialog = () => {
|
||||
showReasonDialog.value = false;
|
||||
};
|
||||
|
||||
const submitApproval = () => {
|
||||
const text = reasonText.value.trim();
|
||||
if (!text) {
|
||||
uni.showToast({ title: '请先填写理由', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
const action = actionType.value;
|
||||
const actionText = action === 'approved' ? '通过' : '拒绝';
|
||||
|
||||
// 这里可以调用API提交备注与状态
|
||||
approvalDetail.value.status = action;
|
||||
approvalDetail.value.approveTime = new Date().toLocaleString('zh-CN');
|
||||
approvalDetail.value.approver = '当前用户'; // 实际应为当前登录用户
|
||||
approvalDetail.value.remark = text;
|
||||
|
||||
showReasonDialog.value = false;
|
||||
uni.showToast({ title: `已${actionText}`, icon: 'success' });
|
||||
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1200);
|
||||
};
|
||||
|
||||
// 头部相关方法
|
||||
const goBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
const showMoreOptions = () => {
|
||||
uni.showActionSheet({
|
||||
itemList: ["刷新", "返回首页"],
|
||||
success: (res) => {
|
||||
switch (res.tapIndex) {
|
||||
case 0:
|
||||
// 刷新数据
|
||||
uni.showToast({ title: "数据已刷新", icon: "success" });
|
||||
break;
|
||||
case 1:
|
||||
uni.reLaunch({
|
||||
url: "/pages/index/index",
|
||||
});
|
||||
uni.showToast({ title: "已返回工作台", icon: "success" });
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 页面加载时获取参数
|
||||
onMounted(() => {
|
||||
const pages = getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
const options = currentPage.options;
|
||||
|
||||
// 从URL参数中获取数据
|
||||
approvalDetail.value = {
|
||||
id: options.id || '',
|
||||
title: decodeURIComponent(options.title || ''),
|
||||
status: options.status || '',
|
||||
applicant: decodeURIComponent(options.applicant || ''),
|
||||
applyTime: decodeURIComponent(options.applyTime || ''),
|
||||
businessType: decodeURIComponent(options.businessType || ''),
|
||||
priority: options.priority || '',
|
||||
amount: options.amount ? decodeURIComponent(options.amount) : '',
|
||||
approver: '待分配',
|
||||
approveTime: '',
|
||||
remark: ''
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../static/css/style.scss";
|
||||
|
||||
// 变量定义
|
||||
$border-radius: 16rpx;
|
||||
$border-radius-small: 12rpx;
|
||||
$spacing-small: 12rpx;
|
||||
$spacing-medium: 20rpx;
|
||||
$spacing-large: 30rpx;
|
||||
$transition: all 0.3s ease;
|
||||
|
||||
.main-data-page {
|
||||
background: var(--background);
|
||||
min-height: 100vh;
|
||||
padding-top: calc(var(--status-bar-height) + 88rpx);
|
||||
padding-bottom: 40rpx;
|
||||
|
||||
.main-container {
|
||||
padding: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 详情卡片
|
||||
.detail-card {
|
||||
background: var(--surface);
|
||||
border-radius: $border-radius;
|
||||
box-shadow: var(--shadow);
|
||||
border: 1rpx solid var(--border-light);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// 详情头部
|
||||
.detail-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: $spacing-large;
|
||||
border-bottom: 1rpx solid var(--border-light);
|
||||
|
||||
.detail-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--title-color);
|
||||
flex: 1;
|
||||
margin-right: $spacing-medium;
|
||||
}
|
||||
|
||||
.detail-status {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
|
||||
&.pending {
|
||||
background: var(--warning-light);
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
&.approved {
|
||||
background: var(--success-light);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
&.rejected {
|
||||
background: var(--error-light);
|
||||
color: var(--error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 详情区域
|
||||
.detail-section {
|
||||
padding: $spacing-large;
|
||||
border-bottom: 1rpx solid var(--border-light);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: var(--title-color);
|
||||
margin-bottom: $spacing-medium;
|
||||
}
|
||||
}
|
||||
|
||||
// 信息列表
|
||||
.info-list {
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: $spacing-small;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 26rpx;
|
||||
color: var(--text-muted);
|
||||
width: 140rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 26rpx;
|
||||
color: var(--text-color);
|
||||
flex: 1;
|
||||
|
||||
&.amount {
|
||||
color: var(--orange);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.priority-tag {
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: $border-radius-small;
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
|
||||
&.high {
|
||||
background: var(--error-light);
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
&.medium {
|
||||
background: var(--warning-light);
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
&.low {
|
||||
background: var(--success-light);
|
||||
color: var(--success);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 操作按钮
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: $spacing-medium;
|
||||
padding: $spacing-large;
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
border-radius: $border-radius;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
transition: $transition;
|
||||
|
||||
&.reject-btn {
|
||||
background: var(--error-light);
|
||||
color: var(--error);
|
||||
border: 1rpx solid var(--error);
|
||||
|
||||
&:active {
|
||||
background: var(--error);
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
&.approve-btn {
|
||||
background: var(--success-light);
|
||||
color: var(--success);
|
||||
border: 1rpx solid var(--success);
|
||||
|
||||
&:active {
|
||||
background: var(--success);
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 支持安全区域的设备
|
||||
@supports (padding: max(0px)) {
|
||||
.main-data-page {
|
||||
padding-top: calc(
|
||||
var(--status-bar-height) + 88rpx + env(safe-area-inset-top)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 理由弹窗样式
|
||||
.reason-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.reason-modal {
|
||||
width: 86%;
|
||||
background: var(--surface);
|
||||
border-radius: $border-radius;
|
||||
box-shadow: var(--shadow-lg);
|
||||
border: 1rpx solid var(--border-light);
|
||||
padding: $spacing-large;
|
||||
}
|
||||
|
||||
.reason-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: var(--title-color);
|
||||
margin-bottom: $spacing-medium;
|
||||
}
|
||||
|
||||
.reason-textarea {
|
||||
width: 100%;
|
||||
min-height: 160rpx;
|
||||
background: var(--surface-hover);
|
||||
border: 1rpx solid var(--border-light);
|
||||
border-radius: $border-radius-small;
|
||||
padding: $spacing-medium;
|
||||
font-size: 26rpx;
|
||||
color: var(--text-color);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.reason-tip {
|
||||
margin-top: $spacing-small;
|
||||
font-size: 22rpx;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.reason-actions {
|
||||
margin-top: $spacing-large;
|
||||
display: flex;
|
||||
gap: $spacing-medium;
|
||||
}
|
||||
|
||||
.reason-btn {
|
||||
flex: 1;
|
||||
height: 76rpx;
|
||||
border-radius: $border-radius;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.reason-btn.cancel {
|
||||
background: var(--button-secondary);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.reason-btn.confirm {
|
||||
background: var(--primary-color);
|
||||
color: var(--white);
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<view class="main-data-page">
|
||||
<!-- 使用子头部组件 -->
|
||||
<sub-header
|
||||
title="业务审批"
|
||||
@goBack="goBack"
|
||||
@showMoreOptions="showMoreOptions"
|
||||
/>
|
||||
<view class="main-container">
|
||||
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import SubHeader from "../components/subHeader.vue";
|
||||
|
||||
// 头部相关方法
|
||||
const goBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
const showMoreOptions = () => {
|
||||
uni.showActionSheet({
|
||||
itemList: ["刷新", "返回首页"],
|
||||
success: (res) => {
|
||||
switch (res.tapIndex) {
|
||||
case 0:
|
||||
// 刷新数据
|
||||
uni.reLaunch({
|
||||
url: "/pages/businessDatas/index",
|
||||
});
|
||||
uni.showToast({ title: "数据已刷新", icon: "success" });
|
||||
break;
|
||||
case 1:
|
||||
uni.reLaunch({
|
||||
url: "/pages/index/index",
|
||||
});
|
||||
uni.showToast({ title: "已返回工作台", icon: "success" });
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.main-data-page {
|
||||
background: #f9f9fb;
|
||||
min-height: 100vh;
|
||||
padding-top: calc(var(--status-bar-height) + 88rpx);
|
||||
padding-bottom: 40rpx;
|
||||
|
||||
.main-container{
|
||||
padding: 24rpx;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/* 支持安全区域的设备 */
|
||||
@supports (padding: max(0px)) {
|
||||
.business-data-page {
|
||||
padding-top: calc(
|
||||
var(--status-bar-height) + 88rpx + env(safe-area-inset-top)
|
||||
);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user