created
This commit is contained in:
5
src/pages/health/chronic/diabetes.vue
Normal file
5
src/pages/health/chronic/diabetes.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template></template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
5
src/pages/health/chronic/hypertension.vue
Normal file
5
src/pages/health/chronic/hypertension.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template></template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
1159
src/pages/health/chronic/index.vue
Normal file
1159
src/pages/health/chronic/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
532
src/pages/health/constitution/index.vue
Normal file
532
src/pages/health/constitution/index.vue
Normal file
@ -0,0 +1,532 @@
|
||||
<template>
|
||||
<view class="constitution-identification">
|
||||
<!-- 当前体质状态 -->
|
||||
<view v-if="userConstitution" class="current-constitution">
|
||||
<view class="constitution-card">
|
||||
<view class="constitution-header">
|
||||
<text class="constitution-name">{{ userConstitution.name }}</text>
|
||||
<text class="constitution-date">{{ formatDate(userConstitution.testDate) }}</text>
|
||||
</view>
|
||||
<view class="constitution-description">
|
||||
<text>{{ userConstitution.description }}</text>
|
||||
</view>
|
||||
<view class="constitution-characteristics">
|
||||
<text class="characteristics-title">主要特征:</text>
|
||||
<view class="characteristics-list">
|
||||
<text
|
||||
v-for="item in userConstitution.characteristics"
|
||||
:key="item"
|
||||
class="characteristic-item"
|
||||
>
|
||||
{{ item }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="constitution-score">
|
||||
<text class="score-label">体质得分:</text>
|
||||
<text class="score-value">{{ userConstitution.score }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 体质调理建议 -->
|
||||
<view class="constitution-advice">
|
||||
<view class="section-title">调理建议</view>
|
||||
<view class="advice-tabs">
|
||||
<view
|
||||
v-for="tab in adviceTabs"
|
||||
:key="tab.key"
|
||||
class="advice-tab"
|
||||
:class="{ active: activeAdviceTab === tab.key }"
|
||||
@click="switchAdviceTab(tab.key)"
|
||||
>
|
||||
<text>{{ tab.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="advice-content">
|
||||
<view v-for="advice in currentAdvices" :key="advice.id" class="advice-item">
|
||||
<view class="advice-header">
|
||||
<text class="advice-title">{{ advice.title }}</text>
|
||||
<text class="advice-level" :class="advice.level">{{ advice.levelText }}</text>
|
||||
</view>
|
||||
<text class="advice-description">{{ advice.description }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 九种体质介绍 -->
|
||||
<view class="constitution-types">
|
||||
<view class="section-title">九种体质介绍</view>
|
||||
<view class="constitution-grid">
|
||||
<view
|
||||
v-for="type in constitutionTypes"
|
||||
:key="type.id"
|
||||
class="constitution-type-card"
|
||||
@click="viewConstitutionDetail(type)"
|
||||
>
|
||||
<view class="type-icon" :style="{ backgroundColor: type.color }">
|
||||
<text>{{ type.name.charAt(0) }}</text>
|
||||
</view>
|
||||
<text class="type-name">{{ type.name }}</text>
|
||||
<text class="type-description">{{ type.shortDesc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作 -->
|
||||
<view class="bottom-actions">
|
||||
<button class="action-btn primary" @click="startTest">重新测试</button>
|
||||
<button class="action-btn secondary" @click="viewHistory">测试历史</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
interface Constitution {
|
||||
name: string;
|
||||
description: string;
|
||||
characteristics: string[];
|
||||
score: number;
|
||||
testDate: Date;
|
||||
}
|
||||
|
||||
// interface AdviceItem {
|
||||
// id: string;
|
||||
// title: string;
|
||||
// description: string;
|
||||
// level: "high" | "medium" | "low";
|
||||
// levelText: string;
|
||||
// type: "diet" | "exercise" | "lifestyle" | "emotion";
|
||||
// }
|
||||
|
||||
interface ConstitutionType {
|
||||
id: string;
|
||||
name: string;
|
||||
shortDesc: string;
|
||||
color: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const userConstitution = ref<Constitution>({
|
||||
name: "气虚体质",
|
||||
description:
|
||||
"气虚体质是指脏腑功能衰弱,气的推动、温煦、防御、固摄和气化功能减退,从而导致某些功能活动低下或衰退,抗病能力下降等衰弱的现象。",
|
||||
characteristics: ["容易疲劳", "气短懒言", "易出汗", "舌淡苔白", "脉虚弱"],
|
||||
score: 75,
|
||||
testDate: new Date(),
|
||||
});
|
||||
|
||||
const adviceTabs = [
|
||||
{ key: "diet", label: "饮食调理" },
|
||||
{ key: "exercise", label: "运动调理" },
|
||||
{ key: "lifestyle", label: "生活调理" },
|
||||
{ key: "emotion", label: "情志调理" },
|
||||
];
|
||||
|
||||
const activeAdviceTab = ref("diet");
|
||||
|
||||
const adviceData = {
|
||||
diet: [
|
||||
{
|
||||
id: "1",
|
||||
title: "补气食材",
|
||||
description: "多食用山药、大枣、蜂蜜、鸡肉等具有补气作用的食物",
|
||||
level: "high",
|
||||
levelText: "重要",
|
||||
type: "diet",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "忌食寒凉",
|
||||
description: "避免生冷寒凉食物,如冰饮、西瓜等",
|
||||
level: "medium",
|
||||
levelText: "注意",
|
||||
type: "diet",
|
||||
},
|
||||
],
|
||||
exercise: [
|
||||
{
|
||||
id: "3",
|
||||
title: "温和运动",
|
||||
description: "适宜散步、太极拳、八段锦等柔和的运动",
|
||||
level: "high",
|
||||
levelText: "推荐",
|
||||
type: "exercise",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
title: "避免剧烈运动",
|
||||
description: "不宜进行大强度、长时间的运动",
|
||||
level: "medium",
|
||||
levelText: "注意",
|
||||
type: "exercise",
|
||||
},
|
||||
],
|
||||
lifestyle: [
|
||||
{
|
||||
id: "5",
|
||||
title: "规律作息",
|
||||
description: "保证充足睡眠,避免熬夜,建立规律的作息时间",
|
||||
level: "high",
|
||||
levelText: "重要",
|
||||
type: "lifestyle",
|
||||
},
|
||||
],
|
||||
emotion: [
|
||||
{
|
||||
id: "6",
|
||||
title: "保持乐观",
|
||||
description: "保持积极乐观的心态,避免过度忧虑",
|
||||
level: "medium",
|
||||
levelText: "建议",
|
||||
type: "emotion",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const constitutionTypes = ref<ConstitutionType[]>([
|
||||
{
|
||||
id: "1",
|
||||
name: "平和质",
|
||||
shortDesc: "体质均衡",
|
||||
color: "#4CAF50",
|
||||
description: "身体健康,精力充沛",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "气虚质",
|
||||
shortDesc: "容易疲劳",
|
||||
color: "#FFC107",
|
||||
description: "脏腑功能衰弱,气不足",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "阳虚质",
|
||||
shortDesc: "畏寒怕冷",
|
||||
color: "#FF9800",
|
||||
description: "阳气不足,机体功能衰退",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "阴虚质",
|
||||
shortDesc: "口干舌燥",
|
||||
color: "#F44336",
|
||||
description: "阴液不足,机体失润",
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
name: "痰湿质",
|
||||
shortDesc: "形体肥胖",
|
||||
color: "#9C27B0",
|
||||
description: "痰湿凝聚,代谢异常",
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
name: "湿热质",
|
||||
shortDesc: "面垢油腻",
|
||||
color: "#E91E63",
|
||||
description: "湿热内蕴,代谢异常",
|
||||
},
|
||||
{
|
||||
id: "7",
|
||||
name: "血瘀质",
|
||||
shortDesc: "肤色晦暗",
|
||||
color: "#3F51B5",
|
||||
description: "血行不畅,瘀血内阻",
|
||||
},
|
||||
{
|
||||
id: "8",
|
||||
name: "气郁质",
|
||||
shortDesc: "情绪低落",
|
||||
color: "#009688",
|
||||
description: "气机郁滞,情志异常",
|
||||
},
|
||||
{ id: "9", name: "特禀质", shortDesc: "先天异常", color: "#795548", description: "先天禀赋异常" },
|
||||
]);
|
||||
|
||||
const currentAdvices = computed(() => {
|
||||
return adviceData[activeAdviceTab.value as keyof typeof adviceData] || [];
|
||||
});
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
return `${year}年${month}月${day}日`;
|
||||
};
|
||||
|
||||
const switchAdviceTab = (tab: string) => {
|
||||
activeAdviceTab.value = tab;
|
||||
};
|
||||
|
||||
const startTest = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/health/constitution/test",
|
||||
});
|
||||
};
|
||||
|
||||
const viewHistory = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/health/constitution/history",
|
||||
});
|
||||
};
|
||||
|
||||
const viewConstitutionDetail = (type: ConstitutionType) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/health/constitution/detail?id=${type.id}`,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.constitution-identification {
|
||||
min-height: 100vh;
|
||||
padding: 20rpx;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.current-constitution {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.constitution-card {
|
||||
padding: 30rpx;
|
||||
color: white;
|
||||
background: linear-gradient(135deg, #4caf50, #45a049);
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.constitution-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.constitution-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.constitution-date {
|
||||
font-size: 22rpx;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.constitution-description {
|
||||
margin-bottom: 20rpx;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.6;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.constitution-characteristics {
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.characteristics-title {
|
||||
margin-bottom: 12rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.characteristics-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
|
||||
.characteristic-item {
|
||||
padding: 8rpx 16rpx;
|
||||
font-size: 22rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.constitution-score {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.score-label {
|
||||
margin-right: 12rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.score-value {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.constitution-advice {
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background: white;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin-bottom: 24rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.advice-tabs {
|
||||
display: flex;
|
||||
padding: 8rpx;
|
||||
margin-bottom: 24rpx;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.advice-tab {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 60rpx;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
border-radius: 8rpx;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.active {
|
||||
color: white;
|
||||
background: #4caf50;
|
||||
}
|
||||
}
|
||||
|
||||
.advice-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.advice-item {
|
||||
padding: 20rpx;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12rpx;
|
||||
|
||||
.advice-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
.advice-title {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.advice-level {
|
||||
padding: 6rpx 12rpx;
|
||||
font-size: 20rpx;
|
||||
border-radius: 16rpx;
|
||||
|
||||
&.high {
|
||||
color: #f44336;
|
||||
background: #ffebee;
|
||||
}
|
||||
|
||||
&.medium {
|
||||
color: #ff9800;
|
||||
background: #fff3e0;
|
||||
}
|
||||
|
||||
&.low {
|
||||
color: #4caf50;
|
||||
background: #e8f5e8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.advice-description {
|
||||
font-size: 24rpx;
|
||||
line-height: 1.5;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.constitution-types {
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background: white;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.constitution-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.constitution-type-card {
|
||||
padding: 20rpx;
|
||||
text-align: center;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12rpx;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.type-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
margin: 0 auto 12rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.type-name {
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.type-description {
|
||||
font-size: 20rpx;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-actions {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
padding: 30rpx 0;
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
border-radius: 44rpx;
|
||||
|
||||
&.primary {
|
||||
color: white;
|
||||
background: #4caf50;
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
color: #666;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
537
src/pages/health/constitution/test.vue
Normal file
537
src/pages/health/constitution/test.vue
Normal file
@ -0,0 +1,537 @@
|
||||
<template>
|
||||
<view class="constitution-test">
|
||||
<!-- 测试进度 -->
|
||||
<view class="test-progress">
|
||||
<view class="progress-bar">
|
||||
<view class="progress-fill" :style="{ width: progressWidth }"></view>
|
||||
</view>
|
||||
<text class="progress-text">{{ currentQuestionIndex + 1 }}/{{ questions.length }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 当前问题 -->
|
||||
<view v-if="currentQuestion" class="question-container">
|
||||
<view class="question-number">第{{ currentQuestionIndex + 1 }}题</view>
|
||||
<view class="question-text">{{ currentQuestion.question }}</view>
|
||||
|
||||
<!-- 选项列表 -->
|
||||
<view class="options-list">
|
||||
<view
|
||||
v-for="(option, index) in currentQuestion.options"
|
||||
:key="index"
|
||||
class="option-item"
|
||||
:class="{ selected: selectedOption === index }"
|
||||
@click="selectOption(index)"
|
||||
>
|
||||
<view class="option-radio">
|
||||
<view v-if="selectedOption === index" class="radio-dot"></view>
|
||||
</view>
|
||||
<text class="option-text">{{ option.text }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 测试结果 -->
|
||||
<view v-if="testCompleted" class="test-result">
|
||||
<view class="result-header">
|
||||
<text class="result-title">测试完成</text>
|
||||
<text class="result-subtitle">您的体质类型是</text>
|
||||
</view>
|
||||
|
||||
<view class="result-constitution">
|
||||
<view class="constitution-icon" :style="{ backgroundColor: result.color }">
|
||||
<text>{{ result.name.charAt(0) }}</text>
|
||||
</view>
|
||||
<text class="constitution-name">{{ result.name }}</text>
|
||||
<text class="constitution-score">得分:{{ result.score }}分</text>
|
||||
</view>
|
||||
|
||||
<view class="result-description">
|
||||
<text>{{ result.description }}</text>
|
||||
</view>
|
||||
|
||||
<view class="result-characteristics">
|
||||
<text class="characteristics-title">主要特征</text>
|
||||
<view class="characteristics-tags">
|
||||
<text v-for="char in result.characteristics" :key="char" class="characteristic-tag">
|
||||
{{ char }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<view class="bottom-actions">
|
||||
<button
|
||||
v-if="!testCompleted && currentQuestionIndex > 0"
|
||||
class="action-btn secondary"
|
||||
@click="previousQuestion"
|
||||
>
|
||||
上一题
|
||||
</button>
|
||||
<button
|
||||
v-if="!testCompleted && selectedOption !== null"
|
||||
class="action-btn primary"
|
||||
@click="nextQuestion"
|
||||
>
|
||||
{{ isLastQuestion ? "完成测试" : "下一题" }}
|
||||
</button>
|
||||
<button v-if="testCompleted" class="action-btn primary full-width" @click="saveResult">
|
||||
保存结果
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
interface QuestionOption {
|
||||
text: string;
|
||||
score: number;
|
||||
constitution: string;
|
||||
}
|
||||
|
||||
interface Question {
|
||||
id: string;
|
||||
question: string;
|
||||
options: QuestionOption[];
|
||||
}
|
||||
|
||||
interface TestResult {
|
||||
name: string;
|
||||
score: number;
|
||||
description: string;
|
||||
characteristics: string[];
|
||||
color: string;
|
||||
}
|
||||
|
||||
const currentQuestionIndex = ref(0);
|
||||
const selectedOption = ref<number | null>(null);
|
||||
const answers = ref<number[]>([]);
|
||||
const testCompleted = ref(false);
|
||||
|
||||
const questions = ref<Question[]>([
|
||||
{
|
||||
id: "1",
|
||||
question: "您平时是否容易疲劳?",
|
||||
options: [
|
||||
{ text: "非常容易疲劳,经常感到乏力", score: 5, constitution: "气虚" },
|
||||
{ text: "比较容易疲劳", score: 3, constitution: "气虚" },
|
||||
{ text: "偶尔疲劳", score: 2, constitution: "平和" },
|
||||
{ text: "很少疲劳,精力充沛", score: 1, constitution: "平和" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
question: "您的睡眠质量如何?",
|
||||
options: [
|
||||
{ text: "经常失眠,睡眠质量很差", score: 5, constitution: "阴虚" },
|
||||
{ text: "睡眠较浅,容易醒", score: 4, constitution: "阴虚" },
|
||||
{ text: "睡眠一般", score: 2, constitution: "平和" },
|
||||
{ text: "睡眠很好,很少失眠", score: 1, constitution: "平和" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
question: "您对寒冷的感受如何?",
|
||||
options: [
|
||||
{ text: "非常怕冷,手脚冰凉", score: 5, constitution: "阳虚" },
|
||||
{ text: "比较怕冷", score: 3, constitution: "阳虚" },
|
||||
{ text: "对温度变化不太敏感", score: 2, constitution: "平和" },
|
||||
{ text: "不怕冷,甚至喜欢凉爽", score: 1, constitution: "湿热" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
question: "您的情绪状态通常如何?",
|
||||
options: [
|
||||
{ text: "经常感到抑郁、烦躁", score: 5, constitution: "气郁" },
|
||||
{ text: "情绪起伏较大", score: 3, constitution: "气郁" },
|
||||
{ text: "情绪比较稳定", score: 2, constitution: "平和" },
|
||||
{ text: "心情愉快,很少烦躁", score: 1, constitution: "平和" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
question: "您的消化功能如何?",
|
||||
options: [
|
||||
{ text: "消化不良,经常腹胀", score: 5, constitution: "痰湿" },
|
||||
{ text: "消化一般,偶有不适", score: 3, constitution: "痰湿" },
|
||||
{ text: "消化功能正常", score: 2, constitution: "平和" },
|
||||
{ text: "消化很好,食欲旺盛", score: 1, constitution: "平和" },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const constitutionResults = {
|
||||
气虚: {
|
||||
name: "气虚体质",
|
||||
description: "脏腑功能衰弱,气的推动、温煦、防御、固摄和气化功能减退",
|
||||
characteristics: ["容易疲劳", "气短懒言", "易出汗", "舌淡苔白"],
|
||||
color: "#FFC107",
|
||||
},
|
||||
阳虚: {
|
||||
name: "阳虚体质",
|
||||
description: "阳气不足,机体功能衰退,物质代谢活动减退",
|
||||
characteristics: ["畏寒怕冷", "手脚冰凉", "精神不振", "舌淡胖"],
|
||||
color: "#FF9800",
|
||||
},
|
||||
阴虚: {
|
||||
name: "阴虚体质",
|
||||
description: "阴液不足,机体失润,以脏腑失养为主要特征",
|
||||
characteristics: ["口干舌燥", "五心烦热", "盗汗", "舌红少苔"],
|
||||
color: "#F44336",
|
||||
},
|
||||
痰湿: {
|
||||
name: "痰湿体质",
|
||||
description: "痰湿凝聚,代谢异常,以形体肥胖为主要特征",
|
||||
characteristics: ["形体肥胖", "腹部肥满", "口黏腻", "舌苔厚腻"],
|
||||
color: "#9C27B0",
|
||||
},
|
||||
湿热: {
|
||||
name: "湿热体质",
|
||||
description: "湿热内蕴,以面垢油腻、口苦、苔黄腻为主要特征",
|
||||
characteristics: ["面部油腻", "口苦口干", "身重困倦", "舌苔黄腻"],
|
||||
color: "#E91E63",
|
||||
},
|
||||
气郁: {
|
||||
name: "气郁体质",
|
||||
description: "气机郁滞,神情抑郁,忧虑脆弱",
|
||||
characteristics: ["情绪低落", "胸胁胀满", "多愁善感", "舌淡红"],
|
||||
color: "#009688",
|
||||
},
|
||||
平和: {
|
||||
name: "平和体质",
|
||||
description: "阴阳气血调和,体质平和,身体健康",
|
||||
characteristics: ["精力充沛", "睡眠良好", "食欲正常", "舌色淡红"],
|
||||
color: "#4CAF50",
|
||||
},
|
||||
};
|
||||
|
||||
const result = ref<TestResult>({
|
||||
name: "",
|
||||
score: 0,
|
||||
description: "",
|
||||
characteristics: [],
|
||||
color: "",
|
||||
});
|
||||
|
||||
const currentQuestion = computed(() => {
|
||||
return questions.value[currentQuestionIndex.value];
|
||||
});
|
||||
|
||||
const progressWidth = computed(() => {
|
||||
if (testCompleted.value) return "100%";
|
||||
return `${((currentQuestionIndex.value + 1) / questions.value.length) * 100}%`;
|
||||
});
|
||||
|
||||
const isLastQuestion = computed(() => {
|
||||
return currentQuestionIndex.value === questions.value.length - 1;
|
||||
});
|
||||
|
||||
const selectOption = (index: number) => {
|
||||
selectedOption.value = index;
|
||||
};
|
||||
|
||||
const nextQuestion = () => {
|
||||
if (selectedOption.value === null) return;
|
||||
|
||||
answers.value[currentQuestionIndex.value] = selectedOption.value;
|
||||
|
||||
if (isLastQuestion.value) {
|
||||
calculateResult();
|
||||
testCompleted.value = true;
|
||||
} else {
|
||||
currentQuestionIndex.value++;
|
||||
selectedOption.value = answers.value[currentQuestionIndex.value] ?? null;
|
||||
}
|
||||
};
|
||||
|
||||
const previousQuestion = () => {
|
||||
if (currentQuestionIndex.value > 0) {
|
||||
currentQuestionIndex.value--;
|
||||
selectedOption.value = answers.value[currentQuestionIndex.value] ?? null;
|
||||
}
|
||||
};
|
||||
|
||||
const calculateResult = () => {
|
||||
const constitutionScores: { [key: string]: number } = {};
|
||||
|
||||
answers.value.forEach((answerIndex, questionIndex) => {
|
||||
const question = questions.value[questionIndex];
|
||||
const selectedAnswer = question.options[answerIndex];
|
||||
const constitution = selectedAnswer.constitution;
|
||||
|
||||
constitutionScores[constitution] =
|
||||
(constitutionScores[constitution] || 0) + selectedAnswer.score;
|
||||
});
|
||||
|
||||
// 找出得分最高的体质
|
||||
let maxScore = 0;
|
||||
let dominantConstitution = "平和";
|
||||
|
||||
Object.entries(constitutionScores).forEach(([constitution, score]) => {
|
||||
if (score > maxScore) {
|
||||
maxScore = score;
|
||||
dominantConstitution = constitution;
|
||||
}
|
||||
});
|
||||
|
||||
const constitutionData =
|
||||
constitutionResults[dominantConstitution as keyof typeof constitutionResults];
|
||||
|
||||
result.value = {
|
||||
name: constitutionData.name,
|
||||
score: Math.min(Math.round((maxScore / (questions.value.length * 5)) * 100), 100),
|
||||
description: constitutionData.description,
|
||||
characteristics: constitutionData.characteristics,
|
||||
color: constitutionData.color,
|
||||
};
|
||||
};
|
||||
|
||||
const saveResult = () => {
|
||||
// 保存测试结果
|
||||
uni.showToast({
|
||||
title: "结果已保存",
|
||||
icon: "success",
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1500);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.constitution-test {
|
||||
min-height: 100vh;
|
||||
padding: 20rpx;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.test-progress {
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background: white;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
.progress-bar {
|
||||
height: 8rpx;
|
||||
margin-bottom: 16rpx;
|
||||
overflow: hidden;
|
||||
background: #f0f0f0;
|
||||
border-radius: 4rpx;
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #4caf50, #45a049);
|
||||
border-radius: 4rpx;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.question-container {
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background: white;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.question-number {
|
||||
margin-bottom: 16rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.question-text {
|
||||
margin-bottom: 30rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.options-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.option-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
background: #f8f9fa;
|
||||
border: 2rpx solid transparent;
|
||||
border-radius: 12rpx;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.selected {
|
||||
background: #e8f5e8;
|
||||
border-color: #4caf50;
|
||||
}
|
||||
|
||||
.option-radio {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
margin-right: 20rpx;
|
||||
border: 2rpx solid #ddd;
|
||||
border-radius: 50%;
|
||||
|
||||
.radio-dot {
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
background: #4caf50;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.option-text {
|
||||
flex: 1;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
&.selected .option-radio {
|
||||
border-color: #4caf50;
|
||||
}
|
||||
}
|
||||
|
||||
.test-result {
|
||||
padding: 40rpx;
|
||||
margin-bottom: 20rpx;
|
||||
text-align: center;
|
||||
background: white;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.result-header {
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.result-title {
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.result-subtitle {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.result-constitution {
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.constitution-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
margin: 0 auto 20rpx;
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.constitution-name {
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.constitution-score {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #4caf50;
|
||||
}
|
||||
}
|
||||
|
||||
.result-description {
|
||||
margin-bottom: 30rpx;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.6;
|
||||
color: #666;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.result-characteristics {
|
||||
text-align: left;
|
||||
|
||||
.characteristics-title {
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.characteristics-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
|
||||
.characteristic-tag {
|
||||
padding: 8rpx 16rpx;
|
||||
font-size: 22rpx;
|
||||
color: #4caf50;
|
||||
background: #e8f5e8;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-actions {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
padding: 30rpx 0;
|
||||
|
||||
.action-btn {
|
||||
height: 88rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
border-radius: 44rpx;
|
||||
|
||||
&:not(.full-width) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.primary {
|
||||
color: white;
|
||||
background: #4caf50;
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
color: #666;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
5
src/pages/health/consultation/chat.vue
Normal file
5
src/pages/health/consultation/chat.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template></template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
5
src/pages/health/consultation/doctor-list.vue
Normal file
5
src/pages/health/consultation/doctor-list.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template></template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
478
src/pages/health/consultation/index.vue
Normal file
478
src/pages/health/consultation/index.vue
Normal file
@ -0,0 +1,478 @@
|
||||
<template>
|
||||
<view class="consultation-page">
|
||||
<!-- 顶部搜索 -->
|
||||
<view class="search-section">
|
||||
<view class="search-bar">
|
||||
<text class="search-icon">🔍</text>
|
||||
<input v-model="searchKeyword" placeholder="搜索医生、科室或疾病" @confirm="handleSearch" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 科室分类 -->
|
||||
<view class="department-section">
|
||||
<view class="section-title">科室分类</view>
|
||||
<view class="department-grid">
|
||||
<view
|
||||
v-for="dept in departments"
|
||||
:key="dept.id"
|
||||
class="department-item"
|
||||
@click="selectDepartment(dept)"
|
||||
>
|
||||
<view class="dept-icon">
|
||||
<text class="icon">{{ dept.icon }}</text>
|
||||
</view>
|
||||
<text class="dept-name">{{ dept.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 在线医生 -->
|
||||
<view class="doctor-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">在线医生</text>
|
||||
<text class="more-link" @click="viewAllDoctors">查看全部</text>
|
||||
</view>
|
||||
<view class="doctor-list">
|
||||
<view
|
||||
v-for="doctor in onlineDoctors"
|
||||
:key="doctor.id"
|
||||
class="doctor-card"
|
||||
@click="selectDoctor(doctor)"
|
||||
>
|
||||
<image class="doctor-avatar" :src="doctor.avatar" mode="aspectFill" />
|
||||
<view class="doctor-info">
|
||||
<view class="doctor-name">{{ doctor.name }}</view>
|
||||
<view class="doctor-title">{{ doctor.title }}</view>
|
||||
<view class="doctor-hospital">{{ doctor.hospital }}</view>
|
||||
<view class="doctor-rating">
|
||||
<text class="rating-text">⭐ {{ doctor.rating }}</text>
|
||||
<text class="experience">{{ doctor.experience }}年经验</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="doctor-price">
|
||||
<text class="price">¥{{ doctor.consultationFee }}</text>
|
||||
<view class="online-status">在线</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 咨询历史 -->
|
||||
<view class="history-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">咨询历史</text>
|
||||
<text class="more-link" @click="viewHistory">查看全部</text>
|
||||
</view>
|
||||
<view class="history-list">
|
||||
<view
|
||||
v-for="consultation in consultationHistory"
|
||||
:key="consultation.id"
|
||||
class="history-item"
|
||||
@click="viewConsultation(consultation)"
|
||||
>
|
||||
<image class="doctor-avatar" :src="consultation.doctorAvatar" mode="aspectFill" />
|
||||
<view class="consultation-info">
|
||||
<view class="doctor-name">{{ consultation.doctorName }}</view>
|
||||
<view class="consultation-time">{{ formatTime(consultation.startTime) }}</view>
|
||||
<view class="consultation-status" :class="consultation.status">
|
||||
{{ getStatusText(consultation.status) }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="consultation-type">
|
||||
<text class="type-icon">{{ getTypeIcon(consultation.type) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快速咨询入口 -->
|
||||
<view class="quick-consultation">
|
||||
<button class="quick-btn" @click="quickConsultation">
|
||||
<text class="btn-icon">⚡</text>
|
||||
<text class="btn-text">快速咨询</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
|
||||
const searchKeyword = ref("");
|
||||
|
||||
const departments = ref([
|
||||
{ id: 1, name: "内科", icon: "🫀" },
|
||||
{ id: 2, name: "外科", icon: "🔪" },
|
||||
{ id: 3, name: "妇科", icon: "👩" },
|
||||
{ id: 4, name: "儿科", icon: "👶" },
|
||||
{ id: 5, name: "骨科", icon: "🦴" },
|
||||
{ id: 6, name: "皮肤科", icon: "🧴" },
|
||||
{ id: 7, name: "眼科", icon: "👁️" },
|
||||
{ id: 8, name: "口腔科", icon: "🦷" },
|
||||
]);
|
||||
|
||||
const onlineDoctors = ref([
|
||||
{
|
||||
id: "1",
|
||||
name: "张医生",
|
||||
title: "主任医师",
|
||||
hospital: "三甲医院",
|
||||
avatar: "/static/images/doctor1.jpg",
|
||||
rating: 4.8,
|
||||
experience: 15,
|
||||
consultationFee: 50,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "李医生",
|
||||
title: "副主任医师",
|
||||
hospital: "二甲医院",
|
||||
avatar: "/static/images/doctor2.jpg",
|
||||
rating: 4.6,
|
||||
experience: 12,
|
||||
consultationFee: 40,
|
||||
},
|
||||
]);
|
||||
|
||||
const consultationHistory = ref([
|
||||
{
|
||||
id: "1",
|
||||
doctorName: "王医生",
|
||||
doctorAvatar: "/static/images/doctor3.jpg",
|
||||
startTime: new Date().toISOString(),
|
||||
status: "completed",
|
||||
type: "text",
|
||||
},
|
||||
]);
|
||||
|
||||
const handleSearch = () => {
|
||||
console.log("搜索:", searchKeyword.value);
|
||||
};
|
||||
|
||||
const selectDepartment = (dept: any) => {
|
||||
uni.navigateTo({ url: `/pages/health/consultation/doctor-list?department=${dept.name}` });
|
||||
};
|
||||
|
||||
const selectDoctor = (doctor: any) => {
|
||||
uni.navigateTo({ url: `/pages/health/consultation/chat?doctorId=${doctor.id}` });
|
||||
};
|
||||
|
||||
const viewAllDoctors = () => {
|
||||
uni.navigateTo({ url: "/pages/health/consultation/doctor-list" });
|
||||
};
|
||||
|
||||
const viewHistory = () => {
|
||||
uni.navigateTo({ url: "/pages/health/consultation/history" });
|
||||
};
|
||||
|
||||
const viewConsultation = (consultation: any) => {
|
||||
uni.navigateTo({ url: `/pages/health/consultation/chat?consultationId=${consultation.id}` });
|
||||
};
|
||||
|
||||
const quickConsultation = () => {
|
||||
uni.navigateTo({ url: "/pages/health/consultation/quick" });
|
||||
};
|
||||
|
||||
const formatTime = (time: string) => {
|
||||
return new Date(time).toLocaleDateString("zh-CN");
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
pending: "待开始",
|
||||
ongoing: "进行中",
|
||||
completed: "已完成",
|
||||
cancelled: "已取消",
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
};
|
||||
|
||||
const getTypeIcon = (type: string) => {
|
||||
const typeMap: Record<string, string> = {
|
||||
text: "💬",
|
||||
voice: "🎤",
|
||||
video: "📹",
|
||||
};
|
||||
return typeMap[type] || "💬";
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.consultation-page {
|
||||
min-height: 100vh;
|
||||
padding: 20rpx;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx 30rpx;
|
||||
background: white;
|
||||
border-radius: 25rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
.search-icon {
|
||||
margin-right: 20rpx;
|
||||
font-size: 30rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.department-section {
|
||||
padding: 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
background: white;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
.section-title {
|
||||
margin-bottom: 30rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.department-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 30rpx;
|
||||
}
|
||||
|
||||
.department-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
background: #f8f9fa;
|
||||
border-radius: 15rpx;
|
||||
transition: background 0.3s;
|
||||
|
||||
&:active {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.dept-icon {
|
||||
margin-bottom: 15rpx;
|
||||
|
||||
.icon {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.dept-name {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.doctor-section,
|
||||
.history-section {
|
||||
padding: 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
background: white;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.more-link {
|
||||
font-size: 26rpx;
|
||||
color: #007aff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.doctor-list {
|
||||
.doctor-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 25rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background: #f8f9fa;
|
||||
border-radius: 15rpx;
|
||||
transition: background 0.3s;
|
||||
|
||||
&:active {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.doctor-avatar {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
margin-right: 20rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.doctor-info {
|
||||
flex: 1;
|
||||
|
||||
.doctor-name {
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.doctor-title {
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.doctor-hospital {
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.doctor-rating {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.rating-text {
|
||||
margin-right: 20rpx;
|
||||
font-size: 22rpx;
|
||||
color: #ff9500;
|
||||
}
|
||||
|
||||
.experience {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.doctor-price {
|
||||
text-align: right;
|
||||
|
||||
.price {
|
||||
display: block;
|
||||
margin-bottom: 10rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #ff3b30;
|
||||
}
|
||||
|
||||
.online-status {
|
||||
padding: 5rpx 15rpx;
|
||||
font-size: 20rpx;
|
||||
color: #34c759;
|
||||
background: #e8f5e8;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.history-list {
|
||||
.history-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 25rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background: #f8f9fa;
|
||||
border-radius: 15rpx;
|
||||
|
||||
.doctor-avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
margin-right: 20rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.consultation-info {
|
||||
flex: 1;
|
||||
|
||||
.doctor-name {
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.consultation-time {
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.consultation-status {
|
||||
padding: 5rpx 15rpx;
|
||||
font-size: 22rpx;
|
||||
border-radius: 10rpx;
|
||||
|
||||
&.completed {
|
||||
color: #34c759;
|
||||
background: #e8f5e8;
|
||||
}
|
||||
|
||||
&.ongoing {
|
||||
color: #ff9500;
|
||||
background: #fff2e8;
|
||||
}
|
||||
|
||||
&.pending {
|
||||
color: #007aff;
|
||||
background: #e8f4fd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.consultation-type {
|
||||
.type-icon {
|
||||
font-size: 30rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quick-consultation {
|
||||
position: fixed;
|
||||
right: 30rpx;
|
||||
bottom: 100rpx;
|
||||
|
||||
.quick-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx 40rpx;
|
||||
background: linear-gradient(45deg, #ff6b6b, #ff8e8e);
|
||||
border: none;
|
||||
border-radius: 50rpx;
|
||||
box-shadow: 0 8rpx 20rpx rgba(255, 107, 107, 0.3);
|
||||
|
||||
.btn-icon {
|
||||
margin-right: 10rpx;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
5
src/pages/health/detection/detection.vue
Normal file
5
src/pages/health/detection/detection.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template></template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
612
src/pages/health/detection/index.vue
Normal file
612
src/pages/health/detection/index.vue
Normal file
@ -0,0 +1,612 @@
|
||||
<template>
|
||||
<view class="detection-page">
|
||||
<!-- 顶部总览 -->
|
||||
<view class="health-overview">
|
||||
<view class="overview-header">
|
||||
<text class="title">健康状态</text>
|
||||
<text class="date">{{ formatDate(new Date()) }}</text>
|
||||
</view>
|
||||
<view class="health-score">
|
||||
<view class="score-circle">
|
||||
<text class="score-text">{{ healthScore }}</text>
|
||||
<text class="score-label">健康评分</text>
|
||||
</view>
|
||||
<view class="score-desc">
|
||||
<text class="status" :class="healthLevel">{{ getHealthStatus(healthScore) }}</text>
|
||||
<text class="tips">{{ getHealthTips(healthScore) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快速检测 -->
|
||||
<view class="quick-test-section">
|
||||
<view class="section-title">快速检测</view>
|
||||
<view class="test-grid">
|
||||
<view v-for="test in quickTests" :key="test.id" class="test-item" @click="startTest(test)">
|
||||
<view class="test-icon">
|
||||
<text class="icon">{{ test.icon }}</text>
|
||||
</view>
|
||||
<text class="test-name">{{ test.name }}</text>
|
||||
<text class="test-desc">{{ test.description }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 生命体征 -->
|
||||
<view class="vital-signs">
|
||||
<view class="section-header">
|
||||
<text class="section-title">生命体征</text>
|
||||
<text class="record-btn" @click="recordVitals">记录</text>
|
||||
</view>
|
||||
<view class="vital-grid">
|
||||
<view
|
||||
v-for="vital in vitalSigns"
|
||||
:key="vital.type"
|
||||
class="vital-item"
|
||||
@click="viewVitalDetail(vital)"
|
||||
>
|
||||
<view class="vital-icon">
|
||||
<text class="icon">{{ vital.icon }}</text>
|
||||
</view>
|
||||
<view class="vital-info">
|
||||
<text class="vital-value">{{ vital.value }}</text>
|
||||
<text class="vital-unit">{{ vital.unit }}</text>
|
||||
<text class="vital-label">{{ vital.label }}</text>
|
||||
</view>
|
||||
<view class="vital-status" :class="vital.status">
|
||||
<text class="status-text">{{ getStatusText(vital.status) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 健康趋势 -->
|
||||
<view class="health-trends">
|
||||
<view class="section-title">健康趋势</view>
|
||||
<view class="trend-chart">
|
||||
<view class="chart-placeholder">
|
||||
<text class="chart-text">📊 健康趋势图表</text>
|
||||
</view>
|
||||
<view class="trend-summary">
|
||||
<view class="trend-item">
|
||||
<text class="trend-label">本周平均</text>
|
||||
<text class="trend-value">良好</text>
|
||||
</view>
|
||||
<view class="trend-item">
|
||||
<text class="trend-label">改善指标</text>
|
||||
<text class="trend-value">3项</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 健康报告 -->
|
||||
<view class="health-reports">
|
||||
<view class="section-header">
|
||||
<text class="section-title">健康报告</text>
|
||||
<text class="more-link" @click="viewAllReports">查看全部</text>
|
||||
</view>
|
||||
<view class="report-list">
|
||||
<view
|
||||
v-for="report in recentReports"
|
||||
:key="report.id"
|
||||
class="report-item"
|
||||
@click="viewReport(report)"
|
||||
>
|
||||
<view class="report-icon">
|
||||
<text class="icon">📋</text>
|
||||
</view>
|
||||
<view class="report-info">
|
||||
<text class="report-title">{{ report.title }}</text>
|
||||
<text class="report-date">{{ formatDate(report.date) }}</text>
|
||||
</view>
|
||||
<view class="report-score" :class="report.level">
|
||||
<text class="score">{{ report.score }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 生成报告按钮 -->
|
||||
<view class="generate-report">
|
||||
<button class="generate-btn" @click="generateReport">
|
||||
<text class="btn-icon">📊</text>
|
||||
<text class="btn-text">生成健康报告</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
|
||||
const healthScore = ref(85);
|
||||
const healthLevel = ref("good");
|
||||
|
||||
const quickTests = ref([
|
||||
{ id: 1, name: "血压测量", icon: "🩸", description: "监测血压状况" },
|
||||
{ id: 2, name: "心率检测", icon: "❤️", description: "实时心率监控" },
|
||||
{ id: 3, name: "体温测量", icon: "🌡️", description: "体温健康检查" },
|
||||
{ id: 4, name: "BMI计算", icon: "⚖️", description: "身体质量指数" },
|
||||
]);
|
||||
|
||||
const vitalSigns = ref([
|
||||
{
|
||||
type: "blood_pressure",
|
||||
label: "血压",
|
||||
value: "120/80",
|
||||
unit: "mmHg",
|
||||
icon: "🩸",
|
||||
status: "normal",
|
||||
},
|
||||
{ type: "heart_rate", label: "心率", value: "72", unit: "bpm", icon: "❤️", status: "normal" },
|
||||
{ type: "temperature", label: "体温", value: "36.5", unit: "°C", icon: "🌡️", status: "normal" },
|
||||
{ type: "weight", label: "体重", value: "65.0", unit: "kg", icon: "⚖️", status: "normal" },
|
||||
]);
|
||||
|
||||
const recentReports = ref([
|
||||
{ id: "1", title: "综合健康报告", date: new Date(), score: 85, level: "good" },
|
||||
{
|
||||
id: "2",
|
||||
title: "基础体检报告",
|
||||
date: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
|
||||
score: 78,
|
||||
level: "normal",
|
||||
},
|
||||
]);
|
||||
|
||||
const startTest = (test: any) => {
|
||||
uni.navigateTo({ url: `/pages/health/detection/${test.type}?testId=${test.id}` });
|
||||
};
|
||||
|
||||
const recordVitals = () => {
|
||||
uni.navigateTo({ url: "/pages/health/detection/vitals" });
|
||||
};
|
||||
|
||||
const viewVitalDetail = (vital: any) => {
|
||||
uni.navigateTo({ url: `/pages/health/detection/vital-detail?type=${vital.type}` });
|
||||
};
|
||||
|
||||
const viewAllReports = () => {
|
||||
uni.navigateTo({ url: "/pages/health/detection/reports" });
|
||||
};
|
||||
|
||||
const viewReport = (report: any) => {
|
||||
uni.navigateTo({ url: `/pages/health/detection/report-detail?id=${report.id}` });
|
||||
};
|
||||
|
||||
const generateReport = () => {
|
||||
uni.showLoading({ title: "生成中..." });
|
||||
setTimeout(() => {
|
||||
uni.hideLoading();
|
||||
uni.navigateTo({ url: "/pages/health/detection/generate-report" });
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleDateString("zh-CN");
|
||||
};
|
||||
|
||||
const getHealthStatus = (score: number) => {
|
||||
if (score >= 90) return "优秀";
|
||||
if (score >= 80) return "良好";
|
||||
if (score >= 70) return "一般";
|
||||
return "需改善";
|
||||
};
|
||||
|
||||
const getHealthTips = (score: number) => {
|
||||
if (score >= 90) return "继续保持健康的生活方式";
|
||||
if (score >= 80) return "整体健康状况良好,建议定期检查";
|
||||
if (score >= 70) return "需要注意某些健康指标";
|
||||
return "建议咨询专业医生意见";
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
normal: "正常",
|
||||
warning: "注意",
|
||||
danger: "异常",
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.detection-page {
|
||||
min-height: 100vh;
|
||||
padding: 20rpx;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.health-overview {
|
||||
padding: 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
color: white;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 20rpx;
|
||||
|
||||
.overview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 24rpx;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.health-score {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.score-circle {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 150rpx;
|
||||
height: 150rpx;
|
||||
margin-right: 40rpx;
|
||||
border: 6rpx solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
|
||||
.score-text {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.score-label {
|
||||
margin-top: 5rpx;
|
||||
font-size: 20rpx;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.score-desc {
|
||||
flex: 1;
|
||||
|
||||
.status {
|
||||
display: block;
|
||||
margin-bottom: 15rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
|
||||
&.good {
|
||||
color: #34c759;
|
||||
}
|
||||
&.normal {
|
||||
color: #ff9500;
|
||||
}
|
||||
&.poor {
|
||||
color: #ff3b30;
|
||||
}
|
||||
}
|
||||
|
||||
.tips {
|
||||
font-size: 24rpx;
|
||||
line-height: 1.4;
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quick-test-section {
|
||||
padding: 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
background: white;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
.section-title {
|
||||
margin-bottom: 30rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.test-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.test-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 30rpx 20rpx;
|
||||
background: #f8f9fa;
|
||||
border-radius: 15rpx;
|
||||
transition: transform 0.3s;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.test-icon {
|
||||
margin-bottom: 15rpx;
|
||||
|
||||
.icon {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.test-name {
|
||||
margin-bottom: 10rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.test-desc {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vital-signs {
|
||||
padding: 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
background: white;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.record-btn {
|
||||
padding: 10rpx 20rpx;
|
||||
font-size: 26rpx;
|
||||
color: #007aff;
|
||||
background: #e8f4fd;
|
||||
border-radius: 15rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.vital-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.vital-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 25rpx;
|
||||
background: #f8f9fa;
|
||||
border-radius: 15rpx;
|
||||
|
||||
.vital-icon {
|
||||
margin-right: 20rpx;
|
||||
|
||||
.icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.vital-info {
|
||||
flex: 1;
|
||||
|
||||
.vital-value {
|
||||
margin-right: 5rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.vital-unit {
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.vital-label {
|
||||
display: block;
|
||||
margin-top: 5rpx;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.vital-status {
|
||||
position: absolute;
|
||||
top: 10rpx;
|
||||
right: 10rpx;
|
||||
padding: 5rpx 10rpx;
|
||||
font-size: 20rpx;
|
||||
border-radius: 8rpx;
|
||||
|
||||
&.normal {
|
||||
color: #34c759;
|
||||
background: #e8f5e8;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: #ff9500;
|
||||
background: #fff2e8;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
color: #ff3b30;
|
||||
background: #ffe8e8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.health-trends {
|
||||
padding: 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
background: white;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
.section-title {
|
||||
margin-bottom: 30rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.trend-chart {
|
||||
.chart-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background: #f8f9fa;
|
||||
border-radius: 15rpx;
|
||||
|
||||
.chart-text {
|
||||
font-size: 32rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.trend-summary {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
|
||||
.trend-item {
|
||||
text-align: center;
|
||||
|
||||
.trend-label {
|
||||
display: block;
|
||||
margin-bottom: 10rpx;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.trend-value {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.health-reports {
|
||||
padding: 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
background: white;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.more-link {
|
||||
font-size: 26rpx;
|
||||
color: #007aff;
|
||||
}
|
||||
}
|
||||
|
||||
.report-list {
|
||||
.report-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 25rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background: #f8f9fa;
|
||||
border-radius: 15rpx;
|
||||
|
||||
.report-icon {
|
||||
margin-right: 20rpx;
|
||||
|
||||
.icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.report-info {
|
||||
flex: 1;
|
||||
|
||||
.report-title {
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.report-date {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.report-score {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
|
||||
&.good {
|
||||
color: #34c759;
|
||||
}
|
||||
&.normal {
|
||||
color: #ff9500;
|
||||
}
|
||||
&.poor {
|
||||
color: #ff3b30;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.generate-report {
|
||||
padding: 30rpx 0;
|
||||
|
||||
.generate-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
padding: 25rpx;
|
||||
background: linear-gradient(45deg, #007aff, #5ac8fa);
|
||||
border: none;
|
||||
border-radius: 50rpx;
|
||||
box-shadow: 0 8rpx 20rpx rgba(0, 122, 255, 0.3);
|
||||
|
||||
.btn-icon {
|
||||
margin-right: 15rpx;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
5
src/pages/health/detection/report.vue
Normal file
5
src/pages/health/detection/report.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template></template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
5
src/pages/health/detection/vitals.vue
Normal file
5
src/pages/health/detection/vitals.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template></template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
454
src/pages/health/diet/index.vue
Normal file
454
src/pages/health/diet/index.vue
Normal file
@ -0,0 +1,454 @@
|
||||
<template>
|
||||
<view class="diet-management">
|
||||
<!-- 今日营养摄入概览 -->
|
||||
<view class="nutrition-summary">
|
||||
<view class="summary-header">
|
||||
<text class="date">{{ formatDate(currentDate) }}</text>
|
||||
<text class="tips">营养摄入概览</text>
|
||||
</view>
|
||||
<view class="nutrition-circle">
|
||||
<view class="circle-item">
|
||||
<view
|
||||
class="circle-progress"
|
||||
:style="{
|
||||
background: `conic-gradient(#4CAF50 0deg ${calorieProgress}deg, #f0f0f0 ${calorieProgress}deg 360deg)`,
|
||||
}"
|
||||
>
|
||||
<view class="circle-content">
|
||||
<text class="value">{{ todayNutrition.calories }}</text>
|
||||
<text class="unit">kcal</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="label">热量</text>
|
||||
</view>
|
||||
<view class="nutrients">
|
||||
<view class="nutrient-item">
|
||||
<text class="nutrient-name">蛋白质</text>
|
||||
<text class="nutrient-value">{{ todayNutrition.protein }}g</text>
|
||||
</view>
|
||||
<view class="nutrient-item">
|
||||
<text class="nutrient-name">脂肪</text>
|
||||
<text class="nutrient-value">{{ todayNutrition.fat }}g</text>
|
||||
</view>
|
||||
<view class="nutrient-item">
|
||||
<text class="nutrient-name">碳水</text>
|
||||
<text class="nutrient-value">{{ todayNutrition.carbs }}g</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 中医体质膳食建议 -->
|
||||
<view class="constitution-diet">
|
||||
<view class="section-title">
|
||||
<text>体质膳食建议</text>
|
||||
<text class="constitution-type">{{ userConstitution }}</text>
|
||||
</view>
|
||||
<view class="diet-suggestions">
|
||||
<view v-for="suggestion in dietSuggestions" :key="suggestion.id" class="suggestion-item">
|
||||
<image :src="suggestion.image" class="suggestion-image" />
|
||||
<view class="suggestion-content">
|
||||
<text class="suggestion-name">{{ suggestion.name }}</text>
|
||||
<text class="suggestion-desc">{{ suggestion.description }}</text>
|
||||
<text class="suggestion-effect">{{ suggestion.effect }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 每日膳食记录 -->
|
||||
<view class="daily-meals">
|
||||
<view class="section-title">今日膳食</view>
|
||||
<view class="meal-list">
|
||||
<view v-for="meal in todayMeals" :key="meal.id" class="meal-item">
|
||||
<view class="meal-header">
|
||||
<text class="meal-time">{{ meal.time }}</text>
|
||||
<text class="meal-calories">{{ meal.calories }}kcal</text>
|
||||
</view>
|
||||
<view class="meal-foods">
|
||||
<text v-for="food in meal.foods" :key="food" class="food-item">{{ food }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="bottom-actions">
|
||||
<button class="action-btn primary" @click="goToRecord">记录饮食</button>
|
||||
<button class="action-btn secondary" @click="viewHistory">查看历史</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
|
||||
interface NutritionData {
|
||||
calories: number;
|
||||
protein: number;
|
||||
fat: number;
|
||||
carbs: number;
|
||||
}
|
||||
|
||||
interface Meal {
|
||||
id: string;
|
||||
time: string;
|
||||
calories: number;
|
||||
foods: string[];
|
||||
}
|
||||
|
||||
interface DietSuggestion {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
effect: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
const currentDate = ref(new Date());
|
||||
const userConstitution = ref("气虚体质");
|
||||
|
||||
const todayNutrition = ref<NutritionData>({
|
||||
calories: 1650,
|
||||
protein: 85,
|
||||
fat: 58,
|
||||
carbs: 205,
|
||||
});
|
||||
|
||||
const todayMeals = ref<Meal[]>([
|
||||
{
|
||||
id: "1",
|
||||
time: "早餐 8:00",
|
||||
calories: 420,
|
||||
foods: ["小米粥", "鸡蛋", "包子", "咸菜"],
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
time: "午餐 12:30",
|
||||
calories: 680,
|
||||
foods: ["米饭", "红烧肉", "青菜", "冬瓜汤"],
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
time: "晚餐 18:00",
|
||||
calories: 550,
|
||||
foods: ["面条", "西红柿鸡蛋", "拌黄瓜"],
|
||||
},
|
||||
]);
|
||||
|
||||
const dietSuggestions = ref<DietSuggestion[]>([
|
||||
{
|
||||
id: "1",
|
||||
name: "山药薏米粥",
|
||||
description: "健脾益气,适合气虚体质",
|
||||
effect: "补气健脾,增强体质",
|
||||
image: "/static/images/diet/yam-porridge.jpg",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "黄芪炖鸡",
|
||||
description: "补气养血,增强免疫力",
|
||||
effect: "益气固表,补虚强身",
|
||||
image: "/static/images/diet/chicken-soup.jpg",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "红枣银耳汤",
|
||||
description: "滋阴润燥,养血安神",
|
||||
effect: "补血养颜,润肺止咳",
|
||||
image: "/static/images/diet/dates-soup.jpg",
|
||||
},
|
||||
]);
|
||||
|
||||
const calorieProgress = computed(() => {
|
||||
const targetCalories = 2000;
|
||||
return Math.min((todayNutrition.value.calories / targetCalories) * 360, 360);
|
||||
});
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
return `${month}月${day}日`;
|
||||
};
|
||||
|
||||
const goToRecord = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/health/diet/record",
|
||||
});
|
||||
};
|
||||
|
||||
const viewHistory = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/health/diet/history",
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 获取用户体质信息和膳食数据
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.diet-management {
|
||||
min-height: 100vh;
|
||||
padding: 20rpx;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.nutrition-summary {
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background: white;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.summary-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.date {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.tips {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.nutrition-circle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.circle-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.circle-progress {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 50%;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
content: "";
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.circle-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
|
||||
.value {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-size: 20rpx;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-top: 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.nutrients {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.nutrient-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-width: 200rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8rpx;
|
||||
|
||||
.nutrient-name {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.nutrient-value {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.constitution-diet {
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background: white;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
|
||||
.constitution-type {
|
||||
padding: 8rpx 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #4caf50;
|
||||
background: #e8f5e8;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.diet-suggestions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.suggestion-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12rpx;
|
||||
|
||||
.suggestion-image {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
margin-right: 20rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.suggestion-content {
|
||||
flex: 1;
|
||||
|
||||
.suggestion-name {
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.suggestion-desc {
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.suggestion-effect {
|
||||
display: block;
|
||||
font-size: 20rpx;
|
||||
color: #4caf50;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.daily-meals {
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background: white;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.meal-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.meal-item {
|
||||
padding: 20rpx;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12rpx;
|
||||
|
||||
.meal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
.meal-time {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.meal-calories {
|
||||
font-size: 24rpx;
|
||||
color: #4caf50;
|
||||
}
|
||||
}
|
||||
|
||||
.meal-foods {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
|
||||
.food-item {
|
||||
padding: 6rpx 12rpx;
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
background: white;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-actions {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
padding: 30rpx 0;
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
border-radius: 44rpx;
|
||||
|
||||
&.primary {
|
||||
color: white;
|
||||
background: #4caf50;
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
color: #666;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
505
src/pages/health/diet/record.vue
Normal file
505
src/pages/health/diet/record.vue
Normal file
@ -0,0 +1,505 @@
|
||||
<template>
|
||||
<view class="diet-record">
|
||||
<!-- 选择餐次 -->
|
||||
<view class="meal-type-selector">
|
||||
<view
|
||||
v-for="type in mealTypes"
|
||||
:key="type.value"
|
||||
class="meal-type-item"
|
||||
:class="{ active: selectedMealType === type.value }"
|
||||
@click="selectMealType(type.value)"
|
||||
>
|
||||
<text>{{ type.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索食物 -->
|
||||
<view class="search-section">
|
||||
<view class="search-bar">
|
||||
<uni-icons type="search" size="18" color="#999"></uni-icons>
|
||||
<input v-model="searchKeyword" placeholder="搜索食物" @input="searchFood" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 常用食物 -->
|
||||
<view v-if="!searchKeyword" class="common-foods">
|
||||
<view class="section-title">常用食物</view>
|
||||
<view class="food-grid">
|
||||
<view
|
||||
v-for="food in commonFoods"
|
||||
:key="food.id"
|
||||
class="food-item"
|
||||
@click="selectFood(food)"
|
||||
>
|
||||
<image :src="food.image" class="food-image" />
|
||||
<text class="food-name">{{ food.name }}</text>
|
||||
<text class="food-calories">{{ food.calories }}kcal/100g</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索结果 -->
|
||||
<view v-if="searchKeyword" class="search-results">
|
||||
<view class="section-title">搜索结果</view>
|
||||
<view class="food-list">
|
||||
<view
|
||||
v-for="food in searchResults"
|
||||
:key="food.id"
|
||||
class="food-item"
|
||||
@click="selectFood(food)"
|
||||
>
|
||||
<image :src="food.image" class="food-image" />
|
||||
<view class="food-info">
|
||||
<text class="food-name">{{ food.name }}</text>
|
||||
<text class="food-nutrition">热量: {{ food.calories }}kcal/100g</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 已选择的食物 -->
|
||||
<view v-if="selectedFoods.length > 0" class="selected-foods">
|
||||
<view class="section-title">已选择食物</view>
|
||||
<view class="selected-list">
|
||||
<view v-for="(food, index) in selectedFoods" :key="index" class="selected-item">
|
||||
<view class="food-info">
|
||||
<text class="food-name">{{ food.name }}</text>
|
||||
<text class="food-amount">{{ food.amount }}g</text>
|
||||
</view>
|
||||
<view class="amount-controls">
|
||||
<button class="control-btn" @click="adjustAmount(index, -10)">-</button>
|
||||
<input v-model.number="food.amount" type="number" class="amount-input" />
|
||||
<button class="control-btn" @click="adjustAmount(index, 10)">+</button>
|
||||
</view>
|
||||
<button class="remove-btn" @click="removeFood(index)">删除</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 营养汇总 -->
|
||||
<view class="nutrition-summary">
|
||||
<text class="summary-title">营养汇总</text>
|
||||
<view class="nutrition-row">
|
||||
<text>总热量: {{ totalNutrition.calories }}kcal</text>
|
||||
<text>蛋白质: {{ totalNutrition.protein }}g</text>
|
||||
</view>
|
||||
<view class="nutrition-row">
|
||||
<text>脂肪: {{ totalNutrition.fat }}g</text>
|
||||
<text>碳水: {{ totalNutrition.carbs }}g</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<view v-if="selectedFoods.length > 0" class="bottom-actions">
|
||||
<button class="save-btn" @click="saveDietRecord">保存记录</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
interface Food {
|
||||
id: string;
|
||||
name: string;
|
||||
calories: number;
|
||||
protein: number;
|
||||
fat: number;
|
||||
carbs: number;
|
||||
image: string;
|
||||
}
|
||||
|
||||
interface SelectedFood extends Food {
|
||||
amount: number;
|
||||
}
|
||||
|
||||
interface MealType {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const mealTypes: MealType[] = [
|
||||
{ value: "breakfast", label: "早餐" },
|
||||
{ value: "lunch", label: "午餐" },
|
||||
{ value: "dinner", label: "晚餐" },
|
||||
{ value: "snack", label: "加餐" },
|
||||
];
|
||||
|
||||
const selectedMealType = ref("breakfast");
|
||||
const searchKeyword = ref("");
|
||||
const selectedFoods = ref<SelectedFood[]>([]);
|
||||
|
||||
const commonFoods = ref<Food[]>([
|
||||
{
|
||||
id: "1",
|
||||
name: "米饭",
|
||||
calories: 116,
|
||||
protein: 2.6,
|
||||
fat: 0.3,
|
||||
carbs: 25.9,
|
||||
image: "/static/images/food/rice.jpg",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "鸡蛋",
|
||||
calories: 144,
|
||||
protein: 13.3,
|
||||
fat: 8.8,
|
||||
carbs: 2.8,
|
||||
image: "/static/images/food/egg.jpg",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "鸡胸肉",
|
||||
calories: 133,
|
||||
protein: 19.4,
|
||||
fat: 5.0,
|
||||
carbs: 2.5,
|
||||
image: "/static/images/food/chicken.jpg",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "西兰花",
|
||||
calories: 34,
|
||||
protein: 4.1,
|
||||
fat: 0.6,
|
||||
carbs: 4.3,
|
||||
image: "/static/images/food/broccoli.jpg",
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
name: "苹果",
|
||||
calories: 52,
|
||||
protein: 0.2,
|
||||
fat: 0.2,
|
||||
carbs: 13.8,
|
||||
image: "/static/images/food/apple.jpg",
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
name: "牛奶",
|
||||
calories: 54,
|
||||
protein: 3.0,
|
||||
fat: 3.2,
|
||||
carbs: 3.4,
|
||||
image: "/static/images/food/milk.jpg",
|
||||
},
|
||||
]);
|
||||
|
||||
const searchResults = ref<Food[]>([]);
|
||||
|
||||
const totalNutrition = computed(() => {
|
||||
return selectedFoods.value.reduce(
|
||||
(total, food) => {
|
||||
const ratio = food.amount / 100;
|
||||
return {
|
||||
calories: Math.round(total.calories + food.calories * ratio),
|
||||
protein: Math.round((total.protein + food.protein * ratio) * 10) / 10,
|
||||
fat: Math.round((total.fat + food.fat * ratio) * 10) / 10,
|
||||
carbs: Math.round((total.carbs + food.carbs * ratio) * 10) / 10,
|
||||
};
|
||||
},
|
||||
{ calories: 0, protein: 0, fat: 0, carbs: 0 }
|
||||
);
|
||||
});
|
||||
|
||||
const selectMealType = (type: string) => {
|
||||
selectedMealType.value = type;
|
||||
};
|
||||
|
||||
const searchFood = () => {
|
||||
if (!searchKeyword.value) {
|
||||
searchResults.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// 模拟搜索结果
|
||||
searchResults.value = commonFoods.value.filter((food) => food.name.includes(searchKeyword.value));
|
||||
};
|
||||
|
||||
const selectFood = (food: Food) => {
|
||||
const selectedFood: SelectedFood = {
|
||||
...food,
|
||||
amount: 100,
|
||||
};
|
||||
selectedFoods.value.push(selectedFood);
|
||||
};
|
||||
|
||||
const adjustAmount = (index: number, delta: number) => {
|
||||
const newAmount = selectedFoods.value[index].amount + delta;
|
||||
if (newAmount > 0) {
|
||||
selectedFoods.value[index].amount = newAmount;
|
||||
}
|
||||
};
|
||||
|
||||
const removeFood = (index: number) => {
|
||||
selectedFoods.value.splice(index, 1);
|
||||
};
|
||||
|
||||
const saveDietRecord = () => {
|
||||
// 保存饮食记录
|
||||
uni.showToast({
|
||||
title: "记录保存成功",
|
||||
icon: "success",
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1500);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.diet-record {
|
||||
min-height: 100vh;
|
||||
padding: 20rpx;
|
||||
padding-bottom: 120rpx;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.meal-type-selector {
|
||||
display: flex;
|
||||
padding: 8rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background: white;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.meal-type-item {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 68rpx;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
border-radius: 12rpx;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.active {
|
||||
color: white;
|
||||
background: #4caf50;
|
||||
}
|
||||
}
|
||||
|
||||
.search-section {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
background: white;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
margin-left: 20rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin-bottom: 20rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.common-foods {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.food-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.food-item {
|
||||
padding: 20rpx;
|
||||
text-align: center;
|
||||
background: white;
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
.food-image {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
margin-bottom: 12rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.food-name {
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.food-calories {
|
||||
font-size: 20rpx;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.search-results {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.food-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.food-list .food-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
background: white;
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
.food-image {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
margin-right: 20rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.food-info {
|
||||
flex: 1;
|
||||
|
||||
.food-name {
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.food-nutrition {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.selected-foods {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.selected-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.selected-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
background: white;
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
.food-info {
|
||||
flex: 1;
|
||||
|
||||
.food-name {
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.food-amount {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.amount-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 20rpx;
|
||||
|
||||
.control-btn {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
background: #f0f0f0;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.amount-input {
|
||||
width: 80rpx;
|
||||
height: 60rpx;
|
||||
margin: 0 12rpx;
|
||||
font-size: 24rpx;
|
||||
text-align: center;
|
||||
border: 1rpx solid #e0e0e0;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.remove-btn {
|
||||
padding: 12rpx 20rpx;
|
||||
font-size: 22rpx;
|
||||
color: white;
|
||||
background: #ff4444;
|
||||
border: none;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.nutrition-summary {
|
||||
padding: 20rpx;
|
||||
background: white;
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
.summary-title {
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.nutrition-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12rpx;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-actions {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 20rpx;
|
||||
background: white;
|
||||
box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
background: #4caf50;
|
||||
border: none;
|
||||
border-radius: 44rpx;
|
||||
}
|
||||
</style>
|
945
src/pages/health/education/index.vue
Normal file
945
src/pages/health/education/index.vue
Normal file
@ -0,0 +1,945 @@
|
||||
<template>
|
||||
<view class="health-education">
|
||||
<!-- 轮播图 -->
|
||||
<view class="banner-section">
|
||||
<swiper class="banner-swiper" indicator-dots circular autoplay>
|
||||
<swiper-item v-for="banner in banners" :key="banner.id">
|
||||
<view class="banner-item" @click="viewContent(banner)">
|
||||
<image :src="banner.image" class="banner-image" />
|
||||
<view class="banner-overlay">
|
||||
<text class="banner-title">{{ banner.title }}</text>
|
||||
<text class="banner-desc">{{ banner.description }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
</view>
|
||||
|
||||
<!-- 分类标签 -->
|
||||
<view class="category-tabs">
|
||||
<scroll-view scroll-x="true" class="tabs-scroll">
|
||||
<view class="tabs-list">
|
||||
<view
|
||||
v-for="tab in educationTabs"
|
||||
:key="tab.id"
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === tab.id }"
|
||||
@click="switchTab(tab.id)"
|
||||
>
|
||||
<text>{{ tab.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 内容列表 -->
|
||||
<view class="content-section">
|
||||
<!-- 专家讲座 -->
|
||||
<view v-if="activeTab === 'expert'" class="expert-lectures">
|
||||
<view class="section-header">
|
||||
<text class="section-title">专家讲座</text>
|
||||
<text class="section-more" @click="viewMore('expert')">更多</text>
|
||||
</view>
|
||||
<view class="lecture-list">
|
||||
<view
|
||||
v-for="lecture in expertLectures"
|
||||
:key="lecture.id"
|
||||
class="lecture-item"
|
||||
@click="playVideo(lecture)"
|
||||
>
|
||||
<view class="lecture-video">
|
||||
<image :src="lecture.cover" class="video-cover" />
|
||||
<view class="play-button">
|
||||
<uni-icons type="play-filled" size="24" color="#fff"></uni-icons>
|
||||
</view>
|
||||
<text class="video-duration">{{ lecture.duration }}</text>
|
||||
</view>
|
||||
<view class="lecture-info">
|
||||
<text class="lecture-title">{{ lecture.title }}</text>
|
||||
<text class="lecture-expert">{{ lecture.expert }}</text>
|
||||
<view class="lecture-meta">
|
||||
<text class="lecture-views">{{ lecture.views }}次观看</text>
|
||||
<text class="lecture-date">{{ formatDate(lecture.publishDate) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 健康资讯 -->
|
||||
<view v-if="activeTab === 'news'" class="health-news">
|
||||
<view class="section-header">
|
||||
<text class="section-title">健康资讯</text>
|
||||
<text class="section-more" @click="viewMore('news')">更多</text>
|
||||
</view>
|
||||
<view class="news-list">
|
||||
<view
|
||||
v-for="news in healthNews"
|
||||
:key="news.id"
|
||||
class="news-item"
|
||||
@click="readArticle(news)"
|
||||
>
|
||||
<image :src="news.image" class="news-image" />
|
||||
<view class="news-content">
|
||||
<text class="news-title">{{ news.title }}</text>
|
||||
<text class="news-summary">{{ news.summary }}</text>
|
||||
<view class="news-meta">
|
||||
<text class="news-source">{{ news.source }}</text>
|
||||
<text class="news-time">{{ formatTime(news.publishTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 养生知识 -->
|
||||
<view v-if="activeTab === 'wellness'" class="wellness-knowledge">
|
||||
<view class="section-header">
|
||||
<text class="section-title">养生知识</text>
|
||||
<text class="section-more" @click="viewMore('wellness')">更多</text>
|
||||
</view>
|
||||
<view class="knowledge-grid">
|
||||
<view
|
||||
v-for="knowledge in wellnessKnowledge"
|
||||
:key="knowledge.id"
|
||||
class="knowledge-card"
|
||||
@click="viewKnowledge(knowledge)"
|
||||
>
|
||||
<image :src="knowledge.image" class="knowledge-image" />
|
||||
<view class="knowledge-info">
|
||||
<text class="knowledge-title">{{ knowledge.title }}</text>
|
||||
<text class="knowledge-tag">{{ knowledge.category }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 食疗配方 -->
|
||||
<view v-if="activeTab === 'diet'" class="diet-recipes">
|
||||
<view class="section-header">
|
||||
<text class="section-title">食疗配方</text>
|
||||
<text class="section-more" @click="viewMore('diet')">更多</text>
|
||||
</view>
|
||||
<view class="recipe-list">
|
||||
<view
|
||||
v-for="recipe in dietRecipes"
|
||||
:key="recipe.id"
|
||||
class="recipe-item"
|
||||
@click="viewRecipe(recipe)"
|
||||
>
|
||||
<image :src="recipe.image" class="recipe-image" />
|
||||
<view class="recipe-content">
|
||||
<text class="recipe-title">{{ recipe.title }}</text>
|
||||
<text class="recipe-effect">{{ recipe.effect }}</text>
|
||||
<view class="recipe-ingredients">
|
||||
<text
|
||||
v-for="ingredient in recipe.ingredients.slice(0, 3)"
|
||||
:key="ingredient"
|
||||
class="ingredient"
|
||||
>
|
||||
{{ ingredient }}
|
||||
</text>
|
||||
<text v-if="recipe.ingredients.length > 3" class="more-ingredients">
|
||||
等{{ recipe.ingredients.length }}种
|
||||
</text>
|
||||
</view>
|
||||
<view class="recipe-meta">
|
||||
<text class="recipe-difficulty">{{ recipe.difficulty }}</text>
|
||||
<text class="recipe-time">{{ recipe.cookTime }}分钟</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 运动指导 -->
|
||||
<view v-if="activeTab === 'exercise'" class="exercise-guide">
|
||||
<view class="section-header">
|
||||
<text class="section-title">运动指导</text>
|
||||
<text class="section-more" @click="viewMore('exercise')">更多</text>
|
||||
</view>
|
||||
<view class="guide-list">
|
||||
<view
|
||||
v-for="guide in exerciseGuides"
|
||||
:key="guide.id"
|
||||
class="guide-item"
|
||||
@click="watchGuide(guide)"
|
||||
>
|
||||
<view class="guide-video">
|
||||
<image :src="guide.cover" class="guide-cover" />
|
||||
<view class="play-icon">
|
||||
<uni-icons type="play-filled" size="20" color="#fff"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
<view class="guide-info">
|
||||
<text class="guide-title">{{ guide.title }}</text>
|
||||
<text class="guide-instructor">{{ guide.instructor }}</text>
|
||||
<view class="guide-tags">
|
||||
<text v-for="tag in guide.tags" :key="tag" class="guide-tag">{{ tag }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
|
||||
interface Banner {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
type: string;
|
||||
targetId: string;
|
||||
}
|
||||
|
||||
interface ExpertLecture {
|
||||
id: string;
|
||||
title: string;
|
||||
expert: string;
|
||||
cover: string;
|
||||
duration: string;
|
||||
views: number;
|
||||
publishDate: Date;
|
||||
videoUrl: string;
|
||||
}
|
||||
|
||||
interface HealthNews {
|
||||
id: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
image: string;
|
||||
source: string;
|
||||
publishTime: Date;
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface WellnessKnowledge {
|
||||
id: string;
|
||||
title: string;
|
||||
image: string;
|
||||
category: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface DietRecipe {
|
||||
id: string;
|
||||
title: string;
|
||||
effect: string;
|
||||
image: string;
|
||||
ingredients: string[];
|
||||
difficulty: string;
|
||||
cookTime: number;
|
||||
steps: string[];
|
||||
}
|
||||
|
||||
interface ExerciseGuide {
|
||||
id: string;
|
||||
title: string;
|
||||
instructor: string;
|
||||
cover: string;
|
||||
tags: string[];
|
||||
videoUrl: string;
|
||||
}
|
||||
|
||||
const activeTab = ref("expert");
|
||||
|
||||
const educationTabs = [
|
||||
{ id: "expert", name: "专家讲座" },
|
||||
{ id: "news", name: "健康资讯" },
|
||||
{ id: "wellness", name: "养生知识" },
|
||||
{ id: "diet", name: "食疗配方" },
|
||||
{ id: "exercise", name: "运动指导" },
|
||||
];
|
||||
|
||||
const banners = ref<Banner[]>([
|
||||
{
|
||||
id: "1",
|
||||
title: "春季养生要点",
|
||||
description: "著名中医专家详解春季如何调理身体",
|
||||
image: "/static/images/banner/spring-health.jpg",
|
||||
type: "video",
|
||||
targetId: "lecture1",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "中医体质辨识指南",
|
||||
description: "了解自己的体质,科学调理身体",
|
||||
image: "/static/images/banner/constitution.jpg",
|
||||
type: "article",
|
||||
targetId: "article1",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
title: "食疗养生经典方",
|
||||
description: "传统食疗配方,日常保健必备",
|
||||
image: "/static/images/banner/diet-therapy.jpg",
|
||||
type: "recipe",
|
||||
targetId: "recipe1",
|
||||
},
|
||||
]);
|
||||
|
||||
const expertLectures = ref<ExpertLecture[]>([
|
||||
{
|
||||
id: "1",
|
||||
title: "春季养肝护肝的中医方法",
|
||||
expert: "李教授 - 中医内科专家",
|
||||
cover: "/static/images/lectures/liver-care.jpg",
|
||||
duration: "25:30",
|
||||
views: 12500,
|
||||
publishDate: new Date("2024-06-20"),
|
||||
videoUrl: "https://example.com/video1.mp4",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "气虚体质的调理要点",
|
||||
expert: "王医生 - 中医体质专家",
|
||||
cover: "/static/images/lectures/qixu-care.jpg",
|
||||
duration: "18:45",
|
||||
views: 8900,
|
||||
publishDate: new Date("2024-06-18"),
|
||||
videoUrl: "https://example.com/video2.mp4",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
title: "中医穴位按摩入门",
|
||||
expert: "张医师 - 针灸推拿专家",
|
||||
cover: "/static/images/lectures/acupoint-massage.jpg",
|
||||
duration: "32:15",
|
||||
views: 15600,
|
||||
publishDate: new Date("2024-06-15"),
|
||||
videoUrl: "https://example.com/video3.mp4",
|
||||
},
|
||||
]);
|
||||
|
||||
const healthNews = ref<HealthNews[]>([
|
||||
{
|
||||
id: "1",
|
||||
title: "世界卫生组织发布最新中医药发展报告",
|
||||
summary: "报告显示中医药在全球范围内的认知度和应用率持续提升",
|
||||
image: "/static/images/news/who-report.jpg",
|
||||
source: "健康时报",
|
||||
publishTime: new Date("2024-06-22"),
|
||||
content: "详细新闻内容...",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "夏季防暑降温的中医智慧",
|
||||
summary: "专家建议通过饮食调理和穴位按摩来预防中暑",
|
||||
image: "/static/images/news/summer-heat.jpg",
|
||||
source: "中医药报",
|
||||
publishTime: new Date("2024-06-21"),
|
||||
content: "详细新闻内容...",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
title: "中医治疗失眠症状有新突破",
|
||||
summary: "最新研究表明针灸结合中药治疗失眠效果显著",
|
||||
image: "/static/images/news/insomnia-treatment.jpg",
|
||||
source: "医学前沿",
|
||||
publishTime: new Date("2024-06-20"),
|
||||
content: "详细新闻内容...",
|
||||
},
|
||||
]);
|
||||
|
||||
const wellnessKnowledge = ref<WellnessKnowledge[]>([
|
||||
{
|
||||
id: "1",
|
||||
title: "四季养生原则",
|
||||
image: "/static/images/wellness/four-seasons.jpg",
|
||||
category: "基础理论",
|
||||
content: "四季养生详细内容...",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "睡眠养生法",
|
||||
image: "/static/images/wellness/sleep-health.jpg",
|
||||
category: "生活养生",
|
||||
content: "睡眠养生详细内容...",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
title: "情志调养",
|
||||
image: "/static/images/wellness/emotion-care.jpg",
|
||||
category: "心理健康",
|
||||
content: "情志调养详细内容...",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
title: "经络养生",
|
||||
image: "/static/images/wellness/meridian.jpg",
|
||||
category: "传统疗法",
|
||||
content: "经络养生详细内容...",
|
||||
},
|
||||
]);
|
||||
|
||||
const dietRecipes = ref<DietRecipe[]>([
|
||||
{
|
||||
id: "1",
|
||||
title: "银耳莲子汤",
|
||||
effect: "滋阴润燥,养心安神",
|
||||
image: "/static/images/recipes/yiner-lotus.jpg",
|
||||
ingredients: ["银耳", "莲子", "冰糖", "红枣"],
|
||||
difficulty: "简单",
|
||||
cookTime: 45,
|
||||
steps: ["银耳泡发...", "莲子去芯...", "煮制过程..."],
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "黄芪当归炖鸡",
|
||||
effect: "补气养血,增强免疫",
|
||||
image: "/static/images/recipes/huangqi-chicken.jpg",
|
||||
ingredients: ["黄芪", "当归", "土鸡", "生姜", "枸杞"],
|
||||
difficulty: "中等",
|
||||
cookTime: 90,
|
||||
steps: ["鸡肉处理...", "药材准备...", "炖制过程..."],
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
title: "山药薏米粥",
|
||||
effect: "健脾益气,祛湿止泻",
|
||||
image: "/static/images/recipes/yam-barley.jpg",
|
||||
ingredients: ["山药", "薏米", "大米", "红豆"],
|
||||
difficulty: "简单",
|
||||
cookTime: 60,
|
||||
steps: ["薏米浸泡...", "山药处理...", "煮粥过程..."],
|
||||
},
|
||||
]);
|
||||
|
||||
const exerciseGuides = ref<ExerciseGuide[]>([
|
||||
{
|
||||
id: "1",
|
||||
title: "太极拳入门教学",
|
||||
instructor: "陈师傅",
|
||||
cover: "/static/images/guides/taichi-basic.jpg",
|
||||
tags: ["太极拳", "入门", "传统运动"],
|
||||
videoUrl: "https://example.com/taichi1.mp4",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "八段锦完整教程",
|
||||
instructor: "李教练",
|
||||
cover: "/static/images/guides/baduanjin.jpg",
|
||||
tags: ["八段锦", "养生功法", "完整版"],
|
||||
videoUrl: "https://example.com/baduanjin.mp4",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
title: "五禽戏健身法",
|
||||
instructor: "王老师",
|
||||
cover: "/static/images/guides/wuqinxi.jpg",
|
||||
tags: ["五禽戏", "健身", "模仿动物"],
|
||||
videoUrl: "https://example.com/wuqinxi.mp4",
|
||||
},
|
||||
]);
|
||||
|
||||
const switchTab = (tabId: string) => {
|
||||
activeTab.value = tabId;
|
||||
};
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
return `${month}月${day}日`;
|
||||
};
|
||||
|
||||
const formatTime = (date: Date) => {
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (days === 0) return "今天";
|
||||
if (days === 1) return "昨天";
|
||||
if (days < 7) return `${days}天前`;
|
||||
return formatDate(date);
|
||||
};
|
||||
|
||||
const viewContent = (banner: Banner) => {
|
||||
if (banner.type === "video") {
|
||||
playVideo({ id: banner.targetId } as ExpertLecture);
|
||||
} else if (banner.type === "article") {
|
||||
readArticle({ id: banner.targetId } as HealthNews);
|
||||
}
|
||||
};
|
||||
|
||||
const playVideo = (lecture: ExpertLecture) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/health/education/video?id=${lecture.id}`,
|
||||
});
|
||||
};
|
||||
|
||||
const readArticle = (news: HealthNews) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/health/education/article?id=${news.id}`,
|
||||
});
|
||||
};
|
||||
|
||||
const viewKnowledge = (knowledge: WellnessKnowledge) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/health/education/knowledge?id=${knowledge.id}`,
|
||||
});
|
||||
};
|
||||
|
||||
const viewRecipe = (recipe: DietRecipe) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/health/education/recipe?id=${recipe.id}`,
|
||||
});
|
||||
};
|
||||
|
||||
const watchGuide = (guide: ExerciseGuide) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/health/education/guide?id=${guide.id}`,
|
||||
});
|
||||
};
|
||||
|
||||
const viewMore = (type: string) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/health/education/list?type=${type}`,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.health-education {
|
||||
min-height: 100vh;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.banner-section {
|
||||
height: 400rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.banner-swiper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.banner-item {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.banner-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.banner-overlay {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 40rpx 30rpx 30rpx;
|
||||
color: white;
|
||||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
|
||||
|
||||
.banner-title {
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.banner-desc {
|
||||
font-size: 24rpx;
|
||||
line-height: 1.4;
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.category-tabs {
|
||||
padding: 20rpx 0;
|
||||
background: white;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.tabs-scroll {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tabs-list {
|
||||
display: flex;
|
||||
gap: 32rpx;
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
padding-bottom: 16rpx;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
border-bottom: 3rpx solid transparent;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.active {
|
||||
font-weight: 600;
|
||||
color: #2196f3;
|
||||
border-bottom-color: #2196f3;
|
||||
}
|
||||
}
|
||||
|
||||
.content-section {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.section-more {
|
||||
font-size: 24rpx;
|
||||
color: #2196f3;
|
||||
}
|
||||
}
|
||||
|
||||
// 专家讲座样式
|
||||
.lecture-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.lecture-item {
|
||||
display: flex;
|
||||
padding: 20rpx;
|
||||
background: white;
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.lecture-video {
|
||||
position: relative;
|
||||
width: 200rpx;
|
||||
height: 120rpx;
|
||||
margin-right: 20rpx;
|
||||
overflow: hidden;
|
||||
border-radius: 8rpx;
|
||||
|
||||
.video-cover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.play-button {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.video-duration {
|
||||
position: absolute;
|
||||
right: 8rpx;
|
||||
bottom: 8rpx;
|
||||
padding: 4rpx 8rpx;
|
||||
font-size: 20rpx;
|
||||
color: white;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.lecture-info {
|
||||
flex: 1;
|
||||
|
||||
.lecture-title {
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.lecture-expert {
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
font-size: 22rpx;
|
||||
color: #2196f3;
|
||||
}
|
||||
|
||||
.lecture-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.lecture-views,
|
||||
.lecture-date {
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 健康资讯样式
|
||||
.news-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.news-item {
|
||||
display: flex;
|
||||
padding: 20rpx;
|
||||
background: white;
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
.news-image {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
margin-right: 20rpx;
|
||||
object-fit: cover;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.news-content {
|
||||
flex: 1;
|
||||
|
||||
.news-title {
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.news-summary {
|
||||
display: block;
|
||||
display: -webkit-box;
|
||||
margin-bottom: 12rpx;
|
||||
overflow: hidden;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.4;
|
||||
color: #666;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.news-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.news-source {
|
||||
font-size: 20rpx;
|
||||
color: #2196f3;
|
||||
}
|
||||
|
||||
.news-time {
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 养生知识样式
|
||||
.knowledge-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.knowledge-card {
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
.knowledge-image {
|
||||
width: 100%;
|
||||
height: 200rpx;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.knowledge-info {
|
||||
padding: 20rpx;
|
||||
|
||||
.knowledge-title {
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.knowledge-tag {
|
||||
padding: 4rpx 12rpx;
|
||||
font-size: 20rpx;
|
||||
color: #2196f3;
|
||||
background: #e3f2fd;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 食疗配方样式
|
||||
.recipe-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.recipe-item {
|
||||
display: flex;
|
||||
padding: 20rpx;
|
||||
background: white;
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
.recipe-image {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
margin-right: 20rpx;
|
||||
object-fit: cover;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.recipe-content {
|
||||
flex: 1;
|
||||
|
||||
.recipe-title {
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.recipe-effect {
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
font-size: 22rpx;
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.recipe-ingredients {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8rpx;
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
.ingredient,
|
||||
.more-ingredients {
|
||||
padding: 4rpx 8rpx;
|
||||
font-size: 20rpx;
|
||||
color: #666;
|
||||
background: #f0f0f0;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.recipe-meta {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
|
||||
.recipe-difficulty,
|
||||
.recipe-time {
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 运动指导样式
|
||||
.guide-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.guide-item {
|
||||
display: flex;
|
||||
padding: 20rpx;
|
||||
background: white;
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
.guide-video {
|
||||
position: relative;
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
margin-right: 20rpx;
|
||||
overflow: hidden;
|
||||
border-radius: 8rpx;
|
||||
|
||||
.guide-cover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.play-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
|
||||
.guide-info {
|
||||
flex: 1;
|
||||
|
||||
.guide-title {
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.guide-instructor {
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
font-size: 22rpx;
|
||||
color: #2196f3;
|
||||
}
|
||||
|
||||
.guide-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8rpx;
|
||||
|
||||
.guide-tag {
|
||||
padding: 4rpx 8rpx;
|
||||
font-size: 20rpx;
|
||||
color: #4caf50;
|
||||
background: #e8f5e8;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
497
src/pages/health/encyclopedia/index.vue
Normal file
497
src/pages/health/encyclopedia/index.vue
Normal file
@ -0,0 +1,497 @@
|
||||
<template>
|
||||
<view class="encyclopedia">
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-section">
|
||||
<view class="search-bar">
|
||||
<uni-icons type="search" size="18" color="#999"></uni-icons>
|
||||
<input v-model="searchKeyword" placeholder="搜索中医知识..." @input="searchContent" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分类导航 -->
|
||||
<view class="category-nav">
|
||||
<scroll-view scroll-x="true" class="category-scroll">
|
||||
<view class="category-list">
|
||||
<view
|
||||
v-for="category in categories"
|
||||
:key="category.id"
|
||||
class="category-item"
|
||||
:class="{ active: activeCategory === category.id }"
|
||||
@click="switchCategory(category.id)"
|
||||
>
|
||||
<view class="category-icon" :style="{ backgroundColor: category.color }">
|
||||
<text>{{ category.name.charAt(0) }}</text>
|
||||
</view>
|
||||
<text class="category-name">{{ category.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<view class="content-area">
|
||||
<!-- 搜索结果 -->
|
||||
<view v-if="searchKeyword && searchResults.length > 0" class="search-results">
|
||||
<view class="section-title">搜索结果</view>
|
||||
<view class="content-list">
|
||||
<view
|
||||
v-for="item in searchResults"
|
||||
:key="item.id"
|
||||
class="content-item"
|
||||
@click="viewDetail(item)"
|
||||
>
|
||||
<image :src="item.image" class="content-image" />
|
||||
<view class="content-info">
|
||||
<text class="content-title">{{ item.title }}</text>
|
||||
<text class="content-desc">{{ item.description }}</text>
|
||||
<view class="content-meta">
|
||||
<text class="content-category">{{ item.categoryName }}</text>
|
||||
<text class="content-views">{{ item.views }}次阅读</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分类内容 -->
|
||||
<view v-else class="category-content">
|
||||
<!-- 热门推荐 -->
|
||||
<view v-if="activeCategory === 'all'" class="hot-section">
|
||||
<view class="section-title">热门推荐</view>
|
||||
<view class="hot-grid">
|
||||
<view
|
||||
v-for="item in hotItems"
|
||||
:key="item.id"
|
||||
class="hot-item"
|
||||
@click="viewDetail(item)"
|
||||
>
|
||||
<image :src="item.image" class="hot-image" />
|
||||
<view class="hot-overlay">
|
||||
<text class="hot-title">{{ item.title }}</text>
|
||||
<text class="hot-views">{{ item.views }}次阅读</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分类列表 -->
|
||||
<view class="category-section">
|
||||
<view class="section-title">{{ getCurrentCategoryName() }}</view>
|
||||
<view class="content-list">
|
||||
<view
|
||||
v-for="item in currentContent"
|
||||
:key="item.id"
|
||||
class="content-item"
|
||||
@click="viewDetail(item)"
|
||||
>
|
||||
<image :src="item.image" class="content-image" />
|
||||
<view class="content-info">
|
||||
<text class="content-title">{{ item.title }}</text>
|
||||
<text class="content-desc">{{ item.description }}</text>
|
||||
<view class="content-meta">
|
||||
<text class="content-date">{{ formatDate(item.publishDate) }}</text>
|
||||
<text class="content-views">{{ item.views }}次阅读</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="content-action">
|
||||
<uni-icons type="right" size="14" color="#999"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
interface Category {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface EncyclopediaItem {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
categoryId: string;
|
||||
categoryName: string;
|
||||
publishDate: Date;
|
||||
views: number;
|
||||
content: string;
|
||||
}
|
||||
|
||||
const searchKeyword = ref("");
|
||||
const activeCategory = ref("all");
|
||||
const searchResults = ref<EncyclopediaItem[]>([]);
|
||||
|
||||
const categories = ref<Category[]>([
|
||||
{ id: "all", name: "全部", color: "#2196F3" },
|
||||
{ id: "herbs", name: "中药材", color: "#4CAF50" },
|
||||
{ id: "acupoints", name: "穴位", color: "#FF9800" },
|
||||
{ id: "constitution", name: "体质", color: "#9C27B0" },
|
||||
{ id: "diet", name: "食疗", color: "#E91E63" },
|
||||
{ id: "exercise", name: "养生", color: "#00BCD4" },
|
||||
{ id: "theory", name: "理论", color: "#795548" },
|
||||
]);
|
||||
|
||||
const encyclopediaData = ref<EncyclopediaItem[]>([
|
||||
{
|
||||
id: "1",
|
||||
title: "人参的功效与作用",
|
||||
description: "人参是名贵中药材,具有大补元气、复脉固脱、补脾益肺等功效",
|
||||
image: "/static/images/herbs/ginseng.jpg",
|
||||
categoryId: "herbs",
|
||||
categoryName: "中药材",
|
||||
publishDate: new Date("2024-06-20"),
|
||||
views: 1250,
|
||||
content: "人参详细介绍...",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "足三里穴位详解",
|
||||
description: "足三里是人体重要穴位,具有调理脾胃、补中益气的作用",
|
||||
image: "/static/images/acupoints/zusanli.jpg",
|
||||
categoryId: "acupoints",
|
||||
categoryName: "穴位",
|
||||
publishDate: new Date("2024-06-19"),
|
||||
views: 980,
|
||||
content: "足三里穴位详细介绍...",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
title: "气虚体质的调理方法",
|
||||
description: "气虚体质表现为容易疲劳、气短懒言,需要通过饮食和运动来调理",
|
||||
image: "/static/images/constitution/qixu.jpg",
|
||||
categoryId: "constitution",
|
||||
categoryName: "体质",
|
||||
publishDate: new Date("2024-06-18"),
|
||||
views: 1580,
|
||||
content: "气虚体质调理详细方法...",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
title: "山药薏米粥的制作与功效",
|
||||
description: "山药薏米粥具有健脾益气、祛湿止泻的功效,适合脾虚湿重者食用",
|
||||
image: "/static/images/diet/yam-porridge.jpg",
|
||||
categoryId: "diet",
|
||||
categoryName: "食疗",
|
||||
publishDate: new Date("2024-06-17"),
|
||||
views: 2100,
|
||||
content: "山药薏米粥制作方法与功效详解...",
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
title: "太极拳的养生原理",
|
||||
description: "太极拳动作柔和缓慢,能够调节气血,增强体质,是很好的养生运动",
|
||||
image: "/static/images/exercise/taichi-theory.jpg",
|
||||
categoryId: "exercise",
|
||||
categoryName: "养生",
|
||||
publishDate: new Date("2024-06-16"),
|
||||
views: 875,
|
||||
content: "太极拳养生原理详细解析...",
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
title: "中医五行学说",
|
||||
description: "五行学说是中医理论的重要组成部分,用于解释人体生理病理现象",
|
||||
image: "/static/images/theory/wuxing.jpg",
|
||||
categoryId: "theory",
|
||||
categoryName: "理论",
|
||||
publishDate: new Date("2024-06-15"),
|
||||
views: 650,
|
||||
content: "五行学说详细介绍...",
|
||||
},
|
||||
{
|
||||
id: "7",
|
||||
title: "黄芪的药用价值",
|
||||
description: "黄芪是常用补气药材,具有补气固表、利尿托毒等功效",
|
||||
image: "/static/images/herbs/huangqi.jpg",
|
||||
categoryId: "herbs",
|
||||
categoryName: "中药材",
|
||||
publishDate: new Date("2024-06-14"),
|
||||
views: 1100,
|
||||
content: "黄芪药用价值详解...",
|
||||
},
|
||||
{
|
||||
id: "8",
|
||||
title: "合谷穴的按摩方法",
|
||||
description: "合谷穴是手阳明大肠经穴位,有清热解表、镇静止痛的作用",
|
||||
image: "/static/images/acupoints/hegu.jpg",
|
||||
categoryId: "acupoints",
|
||||
categoryName: "穴位",
|
||||
publishDate: new Date("2024-06-13"),
|
||||
views: 750,
|
||||
content: "合谷穴按摩方法与功效...",
|
||||
},
|
||||
]);
|
||||
|
||||
const hotItems = computed(() => {
|
||||
return encyclopediaData.value.sort((a, b) => b.views - a.views).slice(0, 4);
|
||||
});
|
||||
|
||||
const currentContent = computed(() => {
|
||||
if (activeCategory.value === "all") {
|
||||
return encyclopediaData.value;
|
||||
}
|
||||
return encyclopediaData.value.filter((item) => item.categoryId === activeCategory.value);
|
||||
});
|
||||
|
||||
const getCurrentCategoryName = () => {
|
||||
const category = categories.value.find((cat) => cat.id === activeCategory.value);
|
||||
return category?.name || "全部";
|
||||
};
|
||||
|
||||
const switchCategory = (categoryId: string) => {
|
||||
activeCategory.value = categoryId;
|
||||
searchKeyword.value = "";
|
||||
searchResults.value = [];
|
||||
};
|
||||
|
||||
const searchContent = () => {
|
||||
if (!searchKeyword.value.trim()) {
|
||||
searchResults.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const keyword = searchKeyword.value.toLowerCase();
|
||||
searchResults.value = encyclopediaData.value.filter(
|
||||
(item) =>
|
||||
item.title.toLowerCase().includes(keyword) || item.description.toLowerCase().includes(keyword)
|
||||
);
|
||||
};
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
return `${month}月${day}日`;
|
||||
};
|
||||
|
||||
const viewDetail = (item: EncyclopediaItem) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/health/encyclopedia/detail?id=${item.id}`,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.encyclopedia {
|
||||
min-height: 100vh;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
padding: 20rpx;
|
||||
background: white;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 72rpx;
|
||||
padding: 0 24rpx;
|
||||
background: #f8f9fa;
|
||||
border-radius: 24rpx;
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
margin-left: 16rpx;
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.category-nav {
|
||||
padding: 20rpx 0;
|
||||
background: white;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.category-scroll {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.category-list {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
|
||||
.category-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 100rpx;
|
||||
|
||||
&.active .category-icon {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&.active .category-name {
|
||||
font-weight: 600;
|
||||
color: #2196f3;
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.category-name {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
.content-area {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin-bottom: 20rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.hot-section {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.hot-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.hot-item {
|
||||
position: relative;
|
||||
height: 200rpx;
|
||||
overflow: hidden;
|
||||
border-radius: 12rpx;
|
||||
|
||||
.hot-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.hot-overlay {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 20rpx;
|
||||
color: white;
|
||||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
|
||||
|
||||
.hot-title {
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.hot-views {
|
||||
font-size: 20rpx;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.content-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
background: white;
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.content-image {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
margin-right: 20rpx;
|
||||
object-fit: cover;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.content-info {
|
||||
flex: 1;
|
||||
|
||||
.content-title {
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.content-desc {
|
||||
display: block;
|
||||
display: -webkit-box;
|
||||
margin-bottom: 12rpx;
|
||||
overflow: hidden;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.4;
|
||||
color: #666;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.content-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.content-category,
|
||||
.content-date {
|
||||
padding: 4rpx 12rpx;
|
||||
font-size: 20rpx;
|
||||
color: #2196f3;
|
||||
background: #e3f2fd;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.content-views {
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
571
src/pages/health/exercise/index.vue
Normal file
571
src/pages/health/exercise/index.vue
Normal file
@ -0,0 +1,571 @@
|
||||
<template>
|
||||
<view class="exercise-management">
|
||||
<!-- 今日运动概览 -->
|
||||
<view class="exercise-summary">
|
||||
<view class="summary-header">
|
||||
<text class="date">{{ formatDate(currentDate) }}</text>
|
||||
<text class="weather">晴 22°C 适宜运动</text>
|
||||
</view>
|
||||
|
||||
<view class="stats-grid">
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ todayStats.steps }}</text>
|
||||
<text class="stat-label">步数</text>
|
||||
<text class="stat-unit">步</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ todayStats.calories }}</text>
|
||||
<text class="stat-label">消耗</text>
|
||||
<text class="stat-unit">kcal</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ todayStats.duration }}</text>
|
||||
<text class="stat-label">时长</text>
|
||||
<text class="stat-unit">分钟</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ todayStats.distance }}</text>
|
||||
<text class="stat-label">距离</text>
|
||||
<text class="stat-unit">公里</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 推荐运动 -->
|
||||
<view class="recommended-exercises">
|
||||
<view class="section-title">
|
||||
<text>推荐运动</text>
|
||||
<text class="constitution-tag">{{ userConstitution }}</text>
|
||||
</view>
|
||||
<view class="exercise-cards">
|
||||
<view
|
||||
v-for="exercise in recommendedExercises"
|
||||
:key="exercise.id"
|
||||
class="exercise-card"
|
||||
@click="startExercise(exercise)"
|
||||
>
|
||||
<image :src="exercise.image" class="exercise-image" />
|
||||
<view class="exercise-info">
|
||||
<text class="exercise-name">{{ exercise.name }}</text>
|
||||
<text class="exercise-desc">{{ exercise.description }}</text>
|
||||
<view class="exercise-meta">
|
||||
<text class="exercise-duration">{{ exercise.duration }}分钟</text>
|
||||
<text class="exercise-calories">{{ exercise.calories }}kcal</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="exercise-action">
|
||||
<uni-icons type="right" size="16" color="#999"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 运动计划 -->
|
||||
<view class="exercise-plan">
|
||||
<view class="section-title">本周计划</view>
|
||||
<view class="plan-calendar">
|
||||
<view
|
||||
v-for="day in weekPlan"
|
||||
:key="day.date"
|
||||
class="calendar-day"
|
||||
:class="{ today: day.isToday, completed: day.completed }"
|
||||
>
|
||||
<text class="day-name">{{ day.dayName }}</text>
|
||||
<text class="day-date">{{ day.date }}</text>
|
||||
<view v-if="day.exercise" class="day-exercise">
|
||||
<text>{{ day.exercise }}</text>
|
||||
</view>
|
||||
<view class="day-status">
|
||||
<uni-icons
|
||||
v-if="day.completed"
|
||||
type="checkmarkempty"
|
||||
size="16"
|
||||
color="#4CAF50"
|
||||
></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最近运动记录 -->
|
||||
<view class="recent-records">
|
||||
<view class="section-title">最近记录</view>
|
||||
<view class="record-list">
|
||||
<view v-for="record in recentRecords" :key="record.id" class="record-item">
|
||||
<view class="record-icon" :style="{ backgroundColor: record.color }">
|
||||
<text>{{ record.type.charAt(0) }}</text>
|
||||
</view>
|
||||
<view class="record-info">
|
||||
<text class="record-name">{{ record.name }}</text>
|
||||
<text class="record-time">{{ record.time }}</text>
|
||||
</view>
|
||||
<view class="record-stats">
|
||||
<text class="record-duration">{{ record.duration }}分钟</text>
|
||||
<text class="record-calories">{{ record.calories }}kcal</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作 -->
|
||||
<view class="bottom-actions">
|
||||
<button class="action-btn primary" @click="quickRecord">快速记录</button>
|
||||
<button class="action-btn secondary" @click="viewHistory">查看历史</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
|
||||
interface ExerciseStats {
|
||||
steps: number;
|
||||
calories: number;
|
||||
duration: number;
|
||||
distance: number;
|
||||
}
|
||||
|
||||
interface RecommendedExercise {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
duration: number;
|
||||
calories: number;
|
||||
image: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface ExerciseRecord {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
duration: number;
|
||||
calories: number;
|
||||
time: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface WeekDay {
|
||||
date: number;
|
||||
dayName: string;
|
||||
isToday: boolean;
|
||||
completed: boolean;
|
||||
exercise?: string;
|
||||
}
|
||||
|
||||
const currentDate = ref(new Date());
|
||||
const userConstitution = ref("气虚体质");
|
||||
|
||||
const todayStats = ref<ExerciseStats>({
|
||||
steps: 6542,
|
||||
calories: 285,
|
||||
duration: 45,
|
||||
distance: 4.2,
|
||||
});
|
||||
|
||||
const recommendedExercises = ref<RecommendedExercise[]>([
|
||||
{
|
||||
id: "1",
|
||||
name: "太极拳",
|
||||
description: "柔和缓慢,适合气虚体质调理",
|
||||
duration: 30,
|
||||
calories: 120,
|
||||
image: "/static/images/exercise/taichi.jpg",
|
||||
type: "traditional",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "八段锦",
|
||||
description: "传统养生功法,强身健体",
|
||||
duration: 20,
|
||||
calories: 80,
|
||||
image: "/static/images/exercise/baduanjin.jpg",
|
||||
type: "traditional",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "散步",
|
||||
description: "温和有氧运动,促进气血循环",
|
||||
duration: 40,
|
||||
calories: 150,
|
||||
image: "/static/images/exercise/walking.jpg",
|
||||
type: "cardio",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "瑜伽",
|
||||
description: "伸展身体,平静心神",
|
||||
duration: 45,
|
||||
calories: 180,
|
||||
image: "/static/images/exercise/yoga.jpg",
|
||||
type: "flexibility",
|
||||
},
|
||||
]);
|
||||
|
||||
const weekPlan = ref<WeekDay[]>([
|
||||
{ date: 18, dayName: "周一", isToday: false, completed: true, exercise: "太极拳" },
|
||||
{ date: 19, dayName: "周二", isToday: false, completed: true, exercise: "散步" },
|
||||
{ date: 20, dayName: "周三", isToday: false, completed: false, exercise: "八段锦" },
|
||||
{ date: 21, dayName: "周四", isToday: true, completed: false, exercise: "瑜伽" },
|
||||
{ date: 22, dayName: "周五", isToday: false, completed: false, exercise: "散步" },
|
||||
{ date: 23, dayName: "周六", isToday: false, completed: false, exercise: "太极拳" },
|
||||
{ date: 24, dayName: "周日", isToday: false, completed: false, exercise: "休息" },
|
||||
]);
|
||||
|
||||
const recentRecords = ref<ExerciseRecord[]>([
|
||||
{
|
||||
id: "1",
|
||||
name: "太极拳练习",
|
||||
type: "传统运动",
|
||||
duration: 30,
|
||||
calories: 120,
|
||||
time: "今天 08:00",
|
||||
color: "#4CAF50",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "晨间散步",
|
||||
type: "有氧运动",
|
||||
duration: 25,
|
||||
calories: 95,
|
||||
time: "昨天 07:30",
|
||||
color: "#2196F3",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "八段锦",
|
||||
type: "传统运动",
|
||||
duration: 20,
|
||||
calories: 80,
|
||||
time: "前天 19:00",
|
||||
color: "#FF9800",
|
||||
},
|
||||
]);
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
const weekDay = ["日", "一", "二", "三", "四", "五", "六"][date.getDay()];
|
||||
return `${month}月${day}日 周${weekDay}`;
|
||||
};
|
||||
|
||||
const startExercise = (exercise: RecommendedExercise) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/health/exercise/detail?id=${exercise.id}`,
|
||||
});
|
||||
};
|
||||
|
||||
const quickRecord = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/health/exercise/record",
|
||||
});
|
||||
};
|
||||
|
||||
const viewHistory = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/health/exercise/history",
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.exercise-management {
|
||||
min-height: 100vh;
|
||||
padding: 20rpx;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.exercise-summary {
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
color: white;
|
||||
background: linear-gradient(135deg, #2196f3, #1976d2);
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(33, 150, 243, 0.3);
|
||||
}
|
||||
|
||||
.summary-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.date {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.weather {
|
||||
font-size: 22rpx;
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
|
||||
.stat-value {
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
display: block;
|
||||
margin-bottom: 4rpx;
|
||||
font-size: 22rpx;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.stat-unit {
|
||||
font-size: 18rpx;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.recommended-exercises {
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background: white;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
|
||||
.constitution-tag {
|
||||
padding: 8rpx 16rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: normal;
|
||||
color: #4caf50;
|
||||
background: #e8f5e8;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.exercise-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.exercise-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12rpx;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.exercise-image {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
margin-right: 20rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.exercise-info {
|
||||
flex: 1;
|
||||
|
||||
.exercise-name {
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.exercise-desc {
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.exercise-meta {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
|
||||
.exercise-duration,
|
||||
.exercise-calories {
|
||||
font-size: 20rpx;
|
||||
color: #4caf50;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.exercise-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.exercise-plan {
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background: white;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.plan-calendar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.calendar-day {
|
||||
position: relative;
|
||||
padding: 16rpx 8rpx;
|
||||
text-align: center;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12rpx;
|
||||
|
||||
&.today {
|
||||
background: #e3f2fd;
|
||||
border: 2rpx solid #2196f3;
|
||||
}
|
||||
|
||||
&.completed {
|
||||
background: #e8f5e8;
|
||||
}
|
||||
|
||||
.day-name {
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 20rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.day-date {
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.day-exercise {
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 18rpx;
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.day-status {
|
||||
position: absolute;
|
||||
top: 8rpx;
|
||||
right: 8rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.recent-records {
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background: white;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.record-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.record-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12rpx;
|
||||
|
||||
.record-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
margin-right: 20rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.record-info {
|
||||
flex: 1;
|
||||
|
||||
.record-name {
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.record-time {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.record-stats {
|
||||
text-align: right;
|
||||
|
||||
.record-duration {
|
||||
display: block;
|
||||
margin-bottom: 4rpx;
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.record-calories {
|
||||
font-size: 20rpx;
|
||||
color: #4caf50;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-actions {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
padding: 30rpx 0;
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
border-radius: 44rpx;
|
||||
|
||||
&.primary {
|
||||
color: white;
|
||||
background: #2196f3;
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
color: #666;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
5
src/pages/health/exercise/record.vue
Normal file
5
src/pages/health/exercise/record.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template></template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
589
src/pages/health/index/index.vue
Normal file
589
src/pages/health/index/index.vue
Normal file
@ -0,0 +1,589 @@
|
||||
<template>
|
||||
<view class="health-dashboard">
|
||||
<!-- 顶部状态栏 -->
|
||||
<view class="health-header">
|
||||
<view class="user-greeting">
|
||||
<text class="greeting-text">您好,{{ userInfo.name || "用户" }}</text>
|
||||
<text class="health-score">健康评分:{{ healthScore }}/100</text>
|
||||
</view>
|
||||
<view class="health-ring">
|
||||
<view class="ring-progress" :style="{ '--progress': healthScore }">
|
||||
<text class="ring-text">{{ healthScore }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快捷功能区 -->
|
||||
<view class="quick-actions">
|
||||
<scroll-view scroll-x class="action-scroll">
|
||||
<view class="action-list">
|
||||
<view
|
||||
v-for="action in quickActions"
|
||||
:key="action.id"
|
||||
class="action-item"
|
||||
@click="navigateToModule(action.route)"
|
||||
>
|
||||
<view class="action-icon">
|
||||
<text class="icon">{{ action.icon }}</text>
|
||||
</view>
|
||||
<text class="action-name">{{ action.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 健康数据概览 -->
|
||||
<view class="health-overview">
|
||||
<view class="overview-title">今日健康数据</view>
|
||||
<view class="overview-grid">
|
||||
<view
|
||||
v-for="item in healthData"
|
||||
:key="item.key"
|
||||
class="data-item"
|
||||
@click="navigateToDetail(item.route)"
|
||||
>
|
||||
<view class="data-icon">
|
||||
<text class="icon">{{ item.icon }}</text>
|
||||
</view>
|
||||
<view class="data-content">
|
||||
<view class="data-value-wrapper">
|
||||
<text class="data-value">{{ item.value }}</text>
|
||||
<text class="data-unit">{{ item.unit }}</text>
|
||||
</view>
|
||||
<text class="data-label">{{ item.label }}</text>
|
||||
</view>
|
||||
<view class="data-trend" :class="item.trend">
|
||||
<text>{{ getTrendText(item.trend) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 健康提醒 -->
|
||||
<view v-if="reminders.length > 0" class="health-reminders">
|
||||
<view class="reminder-title">健康提醒</view>
|
||||
<view class="reminder-list">
|
||||
<view
|
||||
v-for="reminder in reminders"
|
||||
:key="reminder.id"
|
||||
class="reminder-item"
|
||||
@click="handleReminder(reminder)"
|
||||
>
|
||||
<view class="reminder-icon" :class="reminder.type">
|
||||
<text class="icon">{{ getReminderIcon(reminder.type) }}</text>
|
||||
</view>
|
||||
<view class="reminder-content">
|
||||
<text class="reminder-text">{{ reminder.message }}</text>
|
||||
<text class="reminder-time">{{ formatTime(reminder.time) }}</text>
|
||||
</view>
|
||||
<view class="reminder-action">
|
||||
<button size="mini" type="primary">处理</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 健康资讯 -->
|
||||
<view class="health-news">
|
||||
<view class="news-header">
|
||||
<text class="news-title">健康资讯</text>
|
||||
<text class="news-more" @click="navigateToEducation">查看更多</text>
|
||||
</view>
|
||||
<view class="news-list">
|
||||
<view
|
||||
v-for="article in healthArticles"
|
||||
:key="article.id"
|
||||
class="news-item"
|
||||
@click="navigateToArticle(article.id)"
|
||||
>
|
||||
<image
|
||||
class="news-image"
|
||||
:src="article.thumbnail || '/static/images/health-default.png'"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="news-content">
|
||||
<text class="news-title">{{ article.title }}</text>
|
||||
<view class="news-meta">
|
||||
<text class="news-category">{{ article.category }}</text>
|
||||
<text class="news-time">{{ formatDate(article.publishDate) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
|
||||
const healthScore = ref(85);
|
||||
|
||||
const quickActions = ref([
|
||||
{ id: 1, name: "在线问诊", icon: "👩⚕️", route: "/pages/health/consultation/index" },
|
||||
{ id: 2, name: "健康检测", icon: "🩺", route: "/pages/health/detection/index" },
|
||||
{ id: 3, name: "慢病管理", icon: "💊", route: "/pages/health/chronic/index" },
|
||||
{ id: 4, name: "膳食管理", icon: "🍎", route: "/pages/health/diet/index" },
|
||||
{ id: 5, name: "运动管理", icon: "🏃♂️", route: "/pages/health/exercise/index" },
|
||||
{ id: 6, name: "体质辩识", icon: "☯️", route: "/pages/health/constitution/index" },
|
||||
{ id: 7, name: "中医百科", icon: "📚", route: "/pages/health/encyclopedia/index" },
|
||||
{ id: 8, name: "健康讲堂", icon: "🎓", route: "/pages/health/education/index" },
|
||||
]);
|
||||
|
||||
const healthData = ref([
|
||||
{
|
||||
key: "heart_rate",
|
||||
label: "心率",
|
||||
value: "72",
|
||||
unit: "bpm",
|
||||
icon: "❤️",
|
||||
trend: "stable",
|
||||
route: "/pages/health/detection/vitals",
|
||||
},
|
||||
{
|
||||
key: "blood_pressure",
|
||||
label: "血压",
|
||||
value: "120/80",
|
||||
unit: "mmHg",
|
||||
icon: "🩸",
|
||||
trend: "down",
|
||||
route: "/pages/health/detection/vitals",
|
||||
},
|
||||
{
|
||||
key: "weight",
|
||||
label: "体重",
|
||||
value: "65.5",
|
||||
unit: "kg",
|
||||
icon: "⚖️",
|
||||
trend: "up",
|
||||
route: "/pages/health/detection/vitals",
|
||||
},
|
||||
{
|
||||
key: "steps",
|
||||
label: "步数",
|
||||
value: "8,234",
|
||||
unit: "步",
|
||||
icon: "👣",
|
||||
trend: "up",
|
||||
route: "/pages/health/exercise/record",
|
||||
},
|
||||
]);
|
||||
|
||||
const reminders = ref([
|
||||
{
|
||||
id: 1,
|
||||
type: "medication",
|
||||
message: "该服用降压药了",
|
||||
time: new Date(),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: "exercise",
|
||||
message: "今日运动目标未完成",
|
||||
time: new Date(),
|
||||
},
|
||||
]);
|
||||
|
||||
const healthArticles = ref([
|
||||
{
|
||||
id: "1",
|
||||
title: "春季养生小贴士",
|
||||
category: "养生保健",
|
||||
publishDate: new Date().toISOString(),
|
||||
thumbnail: "/static/images/article1.jpg",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "如何科学减肥",
|
||||
category: "健康生活",
|
||||
publishDate: new Date().toISOString(),
|
||||
thumbnail: "/static/images/article2.jpg",
|
||||
},
|
||||
]);
|
||||
|
||||
const userInfo = ref({ name: "张三" });
|
||||
|
||||
const navigateToModule = (route: string) => {
|
||||
uni.navigateTo({ url: route });
|
||||
};
|
||||
|
||||
const navigateToDetail = (route: string) => {
|
||||
uni.navigateTo({ url: route });
|
||||
};
|
||||
|
||||
const navigateToEducation = () => {
|
||||
uni.navigateTo({ url: "/pages/health/education/index" });
|
||||
};
|
||||
|
||||
const navigateToArticle = (articleId: string) => {
|
||||
uni.navigateTo({ url: `/pages/health/education/articles/detail?id=${articleId}` });
|
||||
};
|
||||
|
||||
const handleReminder = (reminder: any) => {
|
||||
console.log("处理提醒:", reminder);
|
||||
};
|
||||
|
||||
const getTrendText = (trend: string) => {
|
||||
switch (trend) {
|
||||
case "up":
|
||||
return "↗";
|
||||
case "down":
|
||||
return "↘";
|
||||
default:
|
||||
return "→";
|
||||
}
|
||||
};
|
||||
|
||||
const getReminderIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case "medication":
|
||||
return "💊";
|
||||
case "exercise":
|
||||
return "🏃";
|
||||
case "diet":
|
||||
return "🍎";
|
||||
default:
|
||||
return "⏰";
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (time: Date) => {
|
||||
return time.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" });
|
||||
};
|
||||
|
||||
const formatDate = (date: string) => {
|
||||
return new Date(date).toLocaleDateString("zh-CN");
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.health-dashboard {
|
||||
min-height: 100vh;
|
||||
padding: 20rpx;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.health-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 20rpx;
|
||||
|
||||
.user-greeting {
|
||||
.greeting-text {
|
||||
display: block;
|
||||
margin-bottom: 10rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.health-score {
|
||||
font-size: 28rpx;
|
||||
color: #1aad19;
|
||||
}
|
||||
}
|
||||
|
||||
.health-ring {
|
||||
.ring-progress {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
background: conic-gradient(
|
||||
#1aad19 0deg,
|
||||
#1aad19 calc(var(--progress) * 3.6deg),
|
||||
#e0e0e0 calc(var(--progress) * 3.6deg)
|
||||
);
|
||||
border-radius: 50%;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
content: "";
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.ring-text {
|
||||
z-index: 1;
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.action-scroll {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.action-list {
|
||||
display: flex;
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 140rpx;
|
||||
padding: 30rpx 20rpx;
|
||||
margin-right: 20rpx;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 20rpx;
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
margin-bottom: 15rpx;
|
||||
|
||||
.icon {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.action-name {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.health-overview {
|
||||
padding: 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 20rpx;
|
||||
|
||||
.overview-title {
|
||||
margin-bottom: 30rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.overview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.data-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 25rpx;
|
||||
background: #f8f9fa;
|
||||
border-radius: 15rpx;
|
||||
|
||||
.data-icon {
|
||||
margin-right: 20rpx;
|
||||
|
||||
.icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.data-content {
|
||||
flex: 1;
|
||||
|
||||
.data-value-wrapper {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
.data-value {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.data-unit {
|
||||
margin-left: 5rpx;
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.data-label {
|
||||
display: block;
|
||||
margin-top: 5rpx;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.data-trend {
|
||||
position: absolute;
|
||||
top: 15rpx;
|
||||
right: 15rpx;
|
||||
font-size: 24rpx;
|
||||
|
||||
&.up {
|
||||
color: #ff4757;
|
||||
}
|
||||
&.down {
|
||||
color: #1dd1a1;
|
||||
}
|
||||
&.stable {
|
||||
color: #ffa502;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.health-reminders {
|
||||
padding: 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 20rpx;
|
||||
|
||||
.reminder-title {
|
||||
margin-bottom: 30rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.reminder-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 25rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background: #fff;
|
||||
border-radius: 15rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
.reminder-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
margin-right: 20rpx;
|
||||
border-radius: 50%;
|
||||
|
||||
&.medication {
|
||||
background: #e8f5e8;
|
||||
}
|
||||
&.exercise {
|
||||
background: #e8f4fd;
|
||||
}
|
||||
&.diet {
|
||||
background: #fff2e8;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 30rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.reminder-content {
|
||||
flex: 1;
|
||||
|
||||
.reminder-text {
|
||||
display: block;
|
||||
margin-bottom: 10rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.reminder-time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.health-news {
|
||||
padding: 30rpx;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 20rpx;
|
||||
|
||||
.news-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.news-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.news-more {
|
||||
font-size: 26rpx;
|
||||
color: #007aff;
|
||||
}
|
||||
}
|
||||
|
||||
.news-item {
|
||||
display: flex;
|
||||
padding: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background: #fff;
|
||||
border-radius: 15rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
.news-image {
|
||||
width: 120rpx;
|
||||
height: 90rpx;
|
||||
margin-right: 20rpx;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
.news-content {
|
||||
flex: 1;
|
||||
|
||||
.news-title {
|
||||
display: -webkit-box;
|
||||
margin-bottom: 15rpx;
|
||||
overflow: hidden;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.4;
|
||||
color: #333;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.news-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.news-category,
|
||||
.news-time {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.news-category {
|
||||
padding: 5rpx 15rpx;
|
||||
background: #f0f0f0;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
10
src/pages/health/profile/index.vue
Normal file
10
src/pages/health/profile/index.vue
Normal file
@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<div class="health-index">
|
||||
<h1>Health Index Page</h1>
|
||||
<p>Welcome to the health index page!</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
309
src/pages/index/index.vue
Normal file
309
src/pages/index/index.vue
Normal file
@ -0,0 +1,309 @@
|
||||
<template>
|
||||
<view style="width: 100%; height: var(--status-bar-height)" />
|
||||
<view class="home">
|
||||
<wd-swiper
|
||||
v-model:current="current"
|
||||
:list="swiperList"
|
||||
autoplay
|
||||
@click="handleClick"
|
||||
@change="onChange"
|
||||
/>
|
||||
|
||||
<!-- 快捷导航 -->
|
||||
<wd-grid clickable :column="4" class="mt-2">
|
||||
<wd-grid-item
|
||||
v-for="(item, index) in navList"
|
||||
:key="index"
|
||||
use-slot
|
||||
link-type="navigateTo"
|
||||
:url="item.url"
|
||||
>
|
||||
<view class="p-2">
|
||||
<image class="w-72rpx h-72rpx rounded-8rpx" :src="item.icon" />
|
||||
</view>
|
||||
<view class="text">{{ item.title }}</view>
|
||||
</wd-grid-item>
|
||||
</wd-grid>
|
||||
|
||||
<!-- 通知公告 -->
|
||||
<wd-notice-bar
|
||||
text="中医的慢病管理系统 是一个基于 Vue3 + UniApp + TypeScript 的多端慢病管理系统,支持Android、IOS、鸿蒙、微信小程序等平台,旨在帮助患者更好地管理慢性疾病。 服务器当前维护中,具体上线时间请关注官方通知。"
|
||||
color="#34D19D"
|
||||
type="info"
|
||||
>
|
||||
<template #prefix>
|
||||
<wd-tag color="#FAA21E" bg-color="#FAA21E" plain custom-style="margin-right:10rpx">
|
||||
通知公告
|
||||
</wd-tag>
|
||||
</template>
|
||||
</wd-notice-bar>
|
||||
|
||||
<!-- 数据统计 -->
|
||||
<wd-grid :column="4" :gutter="4">
|
||||
<wd-grid-item use-slot custom-class="custom-item">
|
||||
<view class="flex justify-start pl-5">
|
||||
<view class="flex-center">
|
||||
<image class="w-80rpx h-80rpx rounded-8rpx" src="/static/icons/blood_pressure.png" />
|
||||
<view class="ml-5 text-left">
|
||||
<view class="font-bold">血压</view>
|
||||
<view class="mt-2">
|
||||
{{ VitalSigns.bloodPressure.systolic }}/{{ VitalSigns.bloodPressure.diastolic }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</wd-grid-item>
|
||||
<wd-grid-item use-slot custom-class="custom-item">
|
||||
<view class="flex justify-start pl-5">
|
||||
<view class="flex-center">
|
||||
<image class="w-80rpx h-80rpx rounded-8rpx" src="/static/icons/blood_glucose.png" />
|
||||
<view class="ml-5 text-left">
|
||||
<view class="font-bold">血糖</view>
|
||||
<view class="mt-2">{{ VitalSigns.bloodGlucose }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</wd-grid-item>
|
||||
<wd-grid-item use-slot custom-class="custom-item">
|
||||
<view class="flex justify-start pl-5">
|
||||
<view class="flex-center">
|
||||
<image class="w-80rpx h-80rpx rounded-8rpx" src="/static/icons/spo2.png" />
|
||||
<view class="ml-5 text-left">
|
||||
<view class="font-bold">血氧饱和度</view>
|
||||
<view class="mt-2">{{ VitalSigns.bloodOxygen }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</wd-grid-item>
|
||||
<wd-grid-item use-slot custom-class="custom-item">
|
||||
<view class="flex justify-start pl-5">
|
||||
<view class="flex-center">
|
||||
<image class="w-80rpx h-80rpx rounded-8rpx" src="/static/icons/steps.png" />
|
||||
<view class="ml-5 text-left">
|
||||
<view class="font-bold">今日步数</view>
|
||||
<view class="mt-2">{{ VitalSigns.steps }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</wd-grid-item>
|
||||
</wd-grid>
|
||||
|
||||
<wd-card>
|
||||
<template #title>
|
||||
<view class="flex-between">
|
||||
<view>健康趋势</view>
|
||||
<view>
|
||||
<!-- <wd-radio-group-->
|
||||
<!-- v-model="recentDaysRange"-->
|
||||
<!-- shape="button"-->
|
||||
<!-- inline-->
|
||||
<!-- @change="handleDataRangeChange"-->
|
||||
<!-- >-->
|
||||
<!-- <wd-radio :value="7">近7天</wd-radio>-->
|
||||
<!-- <wd-radio :value="15">近15天</wd-radio>-->
|
||||
<!-- </wd-radio-group>-->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view class="charts-box">
|
||||
<qiun-data-charts type="area" :chartData="chartData" :opts="chartOpts" />
|
||||
</view>
|
||||
</wd-card>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// import { dayjs } from "wot-design-uni";
|
||||
|
||||
import { VitalSignsData } from "@/api/health/detection";
|
||||
const current = ref<number>(0);
|
||||
|
||||
const VitalSigns = ref<VitalSignsData>({
|
||||
heartRate: 75,
|
||||
bodyTemperature: 36.5,
|
||||
weight: 70,
|
||||
height: 175,
|
||||
bloodPressure: {
|
||||
systolic: 120,
|
||||
diastolic: 80,
|
||||
},
|
||||
bloodGlucose: 20,
|
||||
bloodOxygen: 98,
|
||||
steps: 9004,
|
||||
});
|
||||
|
||||
const chartData = ref({
|
||||
categories: ["00:00", "04:00", "08:00", "12:00", "16:00", "20:00"], // 时间点
|
||||
series: [
|
||||
{
|
||||
name: "血压(高压)",
|
||||
color: "#FF6B6B",
|
||||
data: [120, 118, 125, 130, 128, 122],
|
||||
},
|
||||
{
|
||||
name: "血压(低压)",
|
||||
color: "#FFA3A3",
|
||||
data: [80, 78, 82, 85, 83, 79],
|
||||
},
|
||||
{
|
||||
name: "血氧(%)",
|
||||
color: "#4ECDC4",
|
||||
data: [98, 97, 96, 99, 98, 97],
|
||||
},
|
||||
{
|
||||
name: "血糖(mmol/L)",
|
||||
color: "#45B7D1",
|
||||
data: [5.2, 5.0, 6.1, 5.8, 5.5, 5.3],
|
||||
},
|
||||
{
|
||||
name: "步数(千步)",
|
||||
color: "#6A6BFF",
|
||||
data: [0, 0.5, 4.2, 6.7, 8.9, 10.2],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const chartOpts = ref({
|
||||
padding: [20, 20, 30, 40], // 调整padding确保标签可见
|
||||
xAxis: {
|
||||
fontSize: 10,
|
||||
rotateLabel: true,
|
||||
rotateAngle: 45, // 增加角度防止重叠
|
||||
boundaryGap: true, // 让图表从坐标轴开始
|
||||
},
|
||||
yAxis: {
|
||||
disabled: false, // 启用y轴
|
||||
splitNumber: 5,
|
||||
data: [
|
||||
{ min: 0 }, // 步数和血氧从0开始
|
||||
{ min: 70, max: 100 }, // 血氧范围
|
||||
{ min: 4, max: 10 }, // 血糖范围
|
||||
{ min: 70, max: 140 }, // 血压范围
|
||||
],
|
||||
gridType: "dash", // 虚线网格
|
||||
dashLength: 4,
|
||||
},
|
||||
dataLabel: true, // 显示数据标签
|
||||
legend: {
|
||||
position: "bottom", // 图例放在底部
|
||||
fontSize: 10,
|
||||
},
|
||||
extra: {
|
||||
area: {
|
||||
type: "curve",
|
||||
opacity: 0.2,
|
||||
addLine: true,
|
||||
width: 2,
|
||||
gradient: true,
|
||||
activeType: "solid", // 高亮时显示实线
|
||||
animation: true, // 添加动画效果
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 日期范围
|
||||
const recentDaysRange = ref(7);
|
||||
|
||||
const swiperList = ref(["http://115.190.102.167:5324/banner.png"]);
|
||||
|
||||
// 快捷导航列表
|
||||
const navList = reactive([
|
||||
{
|
||||
icon: "/static/icons/user.png",
|
||||
title: "在线问诊",
|
||||
url: "/pages/health/consultation/index",
|
||||
prem: "sys:user:query",
|
||||
},
|
||||
{
|
||||
icon: "/static/icons/role.png",
|
||||
title: "慢病管理",
|
||||
url: "/pages/health/chronic/index",
|
||||
prem: "sys:role:query",
|
||||
},
|
||||
{
|
||||
icon: "/static/icons/notice.png",
|
||||
title: "中医百科",
|
||||
url: "/pages/health/encyclopedia/index",
|
||||
prem: "sys:notice:query",
|
||||
},
|
||||
{
|
||||
icon: "/static/icons/setting.png",
|
||||
title: "健康讲堂",
|
||||
url: "/pages/health/education/index",
|
||||
prem: "sys:config:query",
|
||||
},
|
||||
]);
|
||||
|
||||
function handleClick(e: any) {
|
||||
console.log(e);
|
||||
}
|
||||
function onChange(e: any) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
// 加载访问统计数据
|
||||
// const loadVisitStatsData = async () => {
|
||||
// LogAPI.getVisitStats().then((data) => {
|
||||
// visitStatsData.value = data;
|
||||
// });
|
||||
// };
|
||||
|
||||
// 加载访问趋势数据
|
||||
const loadVisitTrendData = () => {
|
||||
const endDate = new Date();
|
||||
const startDate = new Date(endDate);
|
||||
startDate.setDate(endDate.getDate() - recentDaysRange.value + 1);
|
||||
|
||||
// const visitTrendQuery = {
|
||||
// startDate: dayjs(startDate).format("YYYY-MM-DD"),
|
||||
// endDate: dayjs(endDate).format("YYYY-MM-DD"),
|
||||
// };
|
||||
|
||||
// LogAPI.getVisitTrend(visitTrendQuery).then((data) => {
|
||||
// const res = {
|
||||
// categories: data.dates,
|
||||
// series: [
|
||||
// {
|
||||
// name: "访客数(UV)",
|
||||
// data: data.ipList,
|
||||
// },
|
||||
// {
|
||||
// name: "浏览量(PV)",
|
||||
// data: data.pvList,
|
||||
// },
|
||||
// ],
|
||||
// };
|
||||
// chartData.value = JSON.parse(JSON.stringify(res));
|
||||
// });
|
||||
};
|
||||
|
||||
// // 数据范围变化
|
||||
// const handleDataRangeChange = ({ value }: { value: number }) => {
|
||||
// console.log("handleDataRangeChange", value);
|
||||
// recentDaysRange.value = value;
|
||||
// loadVisitTrendData();
|
||||
// };
|
||||
|
||||
onReady(() => {
|
||||
// loadVisitStatsData();
|
||||
loadVisitTrendData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style setup lang="scss">
|
||||
.home {
|
||||
padding: 10rpx 10rpx;
|
||||
:deep(.custom-item) {
|
||||
height: 80px !important;
|
||||
}
|
||||
:deep(.wd-card) {
|
||||
margin: 10rpx 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.charts-box {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
}
|
||||
</style>
|
393
src/pages/login/index.vue
Normal file
393
src/pages/login/index.vue
Normal file
@ -0,0 +1,393 @@
|
||||
<template>
|
||||
<view class="login-container">
|
||||
<!-- 背景图 -->
|
||||
<image src="/static/images/login-bg.svg" mode="aspectFill" class="login-bg" />
|
||||
|
||||
<!-- Logo和标题区域 -->
|
||||
<view class="header">
|
||||
<image src="/static/logo.png" class="logo" />
|
||||
<text class="title">中医慢病管理系统</text>
|
||||
<text class="subtitle">一种中医慢病管理系统实现方案</text>
|
||||
</view>
|
||||
|
||||
<!-- 登录表单区域 -->
|
||||
<view class="login-card">
|
||||
<view class="form-wrap">
|
||||
<wd-form ref="loginFormRef" :model="loginFormData">
|
||||
<!-- 用户名输入框 -->
|
||||
<view class="form-item">
|
||||
<wd-icon name="user" size="22" color="#165DFF" class="input-icon" />
|
||||
<input v-model="loginFormData.username" class="form-input" placeholder="请输入用户名" />
|
||||
<wd-icon
|
||||
v-if="loginFormData.username"
|
||||
name="close-fill"
|
||||
size="18"
|
||||
color="#9ca3af"
|
||||
class="clear-icon"
|
||||
@click="loginFormData.username = ''"
|
||||
/>
|
||||
</view>
|
||||
<view class="divider"></view>
|
||||
|
||||
<!-- 密码输入框 -->
|
||||
<view class="form-item">
|
||||
<wd-icon name="lock-on" size="22" color="#165DFF" class="input-icon" />
|
||||
<input
|
||||
v-model="loginFormData.password"
|
||||
class="form-input"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
placeholder="请输入密码"
|
||||
placeholder-style="color: #9ca3af; font-weight: normal;"
|
||||
/>
|
||||
<wd-icon
|
||||
:name="showPassword ? 'eye-open' : 'eye-close'"
|
||||
size="18"
|
||||
color="#9ca3af"
|
||||
class="eye-icon"
|
||||
@click="showPassword = !showPassword"
|
||||
/>
|
||||
</view>
|
||||
<view class="divider"></view>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<button
|
||||
class="login-btn"
|
||||
:disabled="loading"
|
||||
:style="loading ? 'opacity: 0.7;' : ''"
|
||||
@click="handleLogin"
|
||||
>
|
||||
登录
|
||||
</button>
|
||||
</wd-form>
|
||||
|
||||
<!-- 微信登录 -->
|
||||
<view class="other-login">
|
||||
<view class="other-login-title">
|
||||
<view class="line"></view>
|
||||
<text class="text">其他登录方式</text>
|
||||
<view class="line"></view>
|
||||
</view>
|
||||
|
||||
<view class="wechat-login" @click="handleWechatLogin">
|
||||
<view class="wechat-icon-wrapper">
|
||||
<image src="/static/icons/weixin.png" class="wechat-icon" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部协议 -->
|
||||
<view class="agreement">
|
||||
<text class="text">登录即同意</text>
|
||||
<text class="link" @click="navigateToUserAgreement">《用户协议》</text>
|
||||
<text class="text">和</text>
|
||||
<text class="link" @click="navigateToPrivacy">《隐私政策》</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<wd-toast />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { type LoginFormData } from "@/api/auth";
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
import { useToast } from "wot-design-uni";
|
||||
import { ref } from "vue";
|
||||
|
||||
const loginFormRef = ref();
|
||||
const toast = useToast();
|
||||
const loading = ref(false);
|
||||
const userStore = useUserStore();
|
||||
const showPassword = ref(false);
|
||||
|
||||
// 登录表单数据
|
||||
const loginFormData = ref<LoginFormData>({
|
||||
username: "admin",
|
||||
password: "123456",
|
||||
});
|
||||
|
||||
// 获取重定向参数
|
||||
const redirect = ref("");
|
||||
onLoad((options) => {
|
||||
if (options) {
|
||||
redirect.value = options.redirect ? decodeURIComponent(options.redirect) : "/pages/index/index";
|
||||
} else {
|
||||
redirect.value = "/pages/index/index";
|
||||
}
|
||||
});
|
||||
|
||||
// 登录处理
|
||||
const handleLogin = () => {
|
||||
if (loading.value) return;
|
||||
loading.value = true;
|
||||
|
||||
userStore
|
||||
.login(loginFormData.value)
|
||||
.then(() => userStore.getInfo())
|
||||
.then(() => {
|
||||
toast.success("登录成功");
|
||||
|
||||
// 检查用户信息是否完整
|
||||
if (!userStore.isUserInfoComplete()) {
|
||||
// 信息不完整,跳转到完善信息页面
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/login/complete-profile?redirect=${encodeURIComponent(redirect.value)}`,
|
||||
});
|
||||
}, 1000);
|
||||
} else {
|
||||
// 否则直接跳转到重定向页面
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({
|
||||
url: redirect.value,
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error?.message || "登录失败");
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
// 微信登录处理
|
||||
const handleWechatLogin = async () => {
|
||||
if (loading.value) return;
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
// #ifdef MP-WEIXIN
|
||||
// 获取微信登录的临时 code
|
||||
const { code } = await uni.login({
|
||||
provider: "weixin",
|
||||
});
|
||||
|
||||
// 调用后端接口进行登录认证
|
||||
const result = await userStore.loginByWechat(code);
|
||||
|
||||
if (result) {
|
||||
// 获取用户信息
|
||||
await userStore.getInfo();
|
||||
toast.success("登录成功");
|
||||
|
||||
// 检查用户信息是否完整
|
||||
if (!userStore.isUserInfoComplete()) {
|
||||
// 如果信息不完整,跳转到完善信息页面
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/login/complete-profile?redirect=${encodeURIComponent(redirect.value)}`,
|
||||
});
|
||||
}, 1000);
|
||||
} else {
|
||||
// 否则直接跳转到重定向页面
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({
|
||||
url: redirect.value,
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
toast.error("当前环境不支持微信登录");
|
||||
// #endif
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || "微信登录失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 跳转到用户协议页面
|
||||
const navigateToUserAgreement = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/mine/user-agreement/index",
|
||||
});
|
||||
};
|
||||
|
||||
// 跳转到隐私政策页面
|
||||
const navigateToPrivacy = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/mine/privacy/index",
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: 120rpx;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 10rpx;
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 28rpx;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 90%;
|
||||
margin-top: 80rpx;
|
||||
overflow: hidden;
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-wrap {
|
||||
padding: 40rpx;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx 0;
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
flex: 1;
|
||||
height: 60rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 60rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.clear-icon,
|
||||
.eye-icon {
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
margin: 0;
|
||||
background-color: rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
height: 90rpx;
|
||||
margin-top: 60rpx;
|
||||
font-size: 32rpx;
|
||||
line-height: 90rpx;
|
||||
color: #fff;
|
||||
background: linear-gradient(90deg, #165dff, #4080ff);
|
||||
border: none;
|
||||
border-radius: 45rpx;
|
||||
box-shadow: 0 8rpx 20rpx rgba(22, 93, 255, 0.3);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.login-btn:active {
|
||||
box-shadow: 0 4rpx 10rpx rgba(22, 93, 255, 0.2);
|
||||
transform: translateY(2rpx);
|
||||
}
|
||||
|
||||
.other-login {
|
||||
margin-top: 60rpx;
|
||||
}
|
||||
|
||||
.other-login-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.line {
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background-color: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.text {
|
||||
padding: 0 30rpx;
|
||||
font-size: 26rpx;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.wechat-login {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.wechat-icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 90rpx;
|
||||
height: 90rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.wechat-icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
}
|
||||
|
||||
.agreement {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 30rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.agreement .text {
|
||||
padding: 0 4rpx;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.agreement .link {
|
||||
color: #165dff;
|
||||
}
|
||||
</style>
|
200
src/pages/mine/about/index.vue
Normal file
200
src/pages/mine/about/index.vue
Normal file
@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<view class="about-container">
|
||||
<!-- 顶部 Logo 区域 -->
|
||||
<view class="logo-section">
|
||||
<image class="logo" src="/static/logo.png" mode="aspectFit" />
|
||||
<text class="app-name">vue-uniapp-template</text>
|
||||
<text class="version">版本 {{ version }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 公司信息区域 -->
|
||||
<view class="company-info">
|
||||
<text class="company-name">有来开源组织</text>
|
||||
<view class="divider" />
|
||||
<text class="company-desc">专注于快速构建和高效开发的应用解决方案</text>
|
||||
</view>
|
||||
|
||||
<!-- 信息列表 -->
|
||||
<view class="info-list">
|
||||
<view class="list-header">
|
||||
<text class="header-title">优质项目</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<view class="item-content">
|
||||
<text class="item-label">vue3-element-admin</text>
|
||||
<text class="item-desc">
|
||||
基于 Vue3 + Vite5+ TypeScript5 + Element-Plus + Pinia
|
||||
等主流技术栈构建的免费开源的中后台管理的前端模板
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<view class="item-content">
|
||||
<text class="item-label">vue-uniapp-template</text>
|
||||
<text class="item-desc">
|
||||
基于 uni-app + Vue 3 + TypeScript 的项目,集成了 ESLint、Prettier、Stylelint、Husky 和
|
||||
Commitlint 等工具,确保代码规范与质量。
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<view class="item-content">
|
||||
<text class="item-label">youlai-boot</text>
|
||||
<text class="item-desc">
|
||||
基于 JDK 17、Spring Boot 3、Spring Security 6、JWT、Redis、Mybatis-Plus、Knife4j、Vue
|
||||
3、Element-Plus 构建的前后端分离单体权限管理系统
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部版权信息 -->
|
||||
<view class="copyright">
|
||||
<text>Copyright © {{ getYear() }} 有来开源组织</text>
|
||||
<text>All Rights Reserved</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const version = ref("1.0.0");
|
||||
const getYear = () => {
|
||||
return new Date().getFullYear();
|
||||
};
|
||||
onMounted(() => {
|
||||
// #ifdef MP-WEIXIN
|
||||
version.value = uni.getSystemInfoSync().appVersion;
|
||||
// #endif
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.about-container {
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.logo-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 30px 0;
|
||||
|
||||
.logo {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
margin-bottom: 8px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.version {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.company-info {
|
||||
padding: 30px;
|
||||
margin: 20px 0;
|
||||
text-align: center;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.company-name {
|
||||
margin-bottom: 15px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 40px;
|
||||
height: 2px;
|
||||
margin: 15px auto;
|
||||
background-color: #409eff;
|
||||
}
|
||||
|
||||
.company-desc {
|
||||
padding: 0 10px;
|
||||
margin-top: 15px;
|
||||
overflow: hidden;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
color: #666;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.info-list {
|
||||
margin: 20px 0;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
|
||||
.list-header {
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.header-title {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
flex: 1;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.item-desc {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.item-value {
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.copyright {
|
||||
padding: 20px 0;
|
||||
text-align: center;
|
||||
|
||||
text {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
</style>
|
154
src/pages/mine/faq/index.vue
Normal file
154
src/pages/mine/faq/index.vue
Normal file
@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<view class="faq-container">
|
||||
<view class="wechat">
|
||||
<view class="tips">
|
||||
<text>长按关注「有来技术」公众号,获取交流群二维码。</text>
|
||||
</view>
|
||||
<view class="flex-center">
|
||||
<image
|
||||
class="w-158px h-158px"
|
||||
:show-menu-by-longpress="true"
|
||||
src="/static/images/qrcode-official.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
<view>
|
||||
<text>如果交流群的二维码过期,请加微信(</text>
|
||||
<text :user-select="true" :selectable="true">haoxianrui</text>
|
||||
<text>)并备注「前端」、「后端」或「全栈」以获取最新二维码。</text>
|
||||
</view>
|
||||
<view>
|
||||
<text>为确保交流群质量,防止营销广告人群混入,我们采取了此措施。望各位理解!</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<wd-collapse v-model="value">
|
||||
<wd-collapse-item title="开源项目issues" name="item1">
|
||||
<!-- #ifdef H5 -->
|
||||
<a href="https://gitee.com/youlaiorg/vue-uniapp-template/issues">#issues</a>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<text :user-select="true">https://gitee.com/youlaiorg/vue-uniapp-template/issues</text>
|
||||
<!-- #endif -->
|
||||
</wd-collapse-item>
|
||||
<wd-collapse-item title="小程序分包" name="item2">
|
||||
<view>
|
||||
<text>
|
||||
分包主要是因为小程序平台对主包大小有限制(微信小程序的规则是主包不超过2M,每个分包不超过2M,总体积一共不能超过20M),
|
||||
分包不需要按照业务模块来分,可以将多个业务模块放入一个分包中,直到这个分包达到小程序的大小限制才考虑下一个分包。
|
||||
uniapp的用法与微信官方文档一样,具体参见:
|
||||
</text>
|
||||
<!-- #ifdef H5 -->
|
||||
<a
|
||||
href="https://developers.weixin.qq.com/miniprogram/dev/framework/subpackages/basic.html"
|
||||
>
|
||||
微信官方文档-分包
|
||||
</a>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<text :user-select="true" :selectable="true">
|
||||
https://developers.weixin.qq.com/miniprogram/dev/framework/subpackages/basic.html
|
||||
</text>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
<view class="mt-15rpx">
|
||||
<text>
|
||||
以下是一个简单示例。以下示例中创建了两个分包,分包a中包含两个页面,分包b中包含一个页面。
|
||||
</text>
|
||||
<text class="mt-15rpx">
|
||||
请注意,如果想把分包页面中使用的组件打包到分包中,则需要将组件放入对应的分包目录下,否则组件会被打包到主包中。
|
||||
</text>
|
||||
</view>
|
||||
<view class="mt-15rpx">
|
||||
<text>目录结构:</text>
|
||||
</view>
|
||||
<rich-text :nodes="subListStr" />
|
||||
<view class="mt-15rpx">
|
||||
<text>在pages.json文件中声明分包结构:</text>
|
||||
</view>
|
||||
<rich-text :nodes="pagesStr" />
|
||||
</wd-collapse-item>
|
||||
</wd-collapse>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const value = ref<string[]>(["item1"]);
|
||||
const subListStr = ref<string>(`
|
||||
<pre style="background-color: #f9f9fa"><code>
|
||||
|-- components //主包组件目录
|
||||
|-- pages //主包页面目录
|
||||
| |-- index
|
||||
|-- sub-pkg-a
|
||||
| |-- components //分包组件目录
|
||||
| |-- pages //分包页面目录
|
||||
| | |-- cat
|
||||
| | |-- dog
|
||||
|-- sub-pkg-b
|
||||
| |-- components //分包组件目录
|
||||
| |-- pages //分包页面目录
|
||||
| | |-- apple</code></pre>
|
||||
`);
|
||||
|
||||
const pagesStr = ref<string>(`<pre style="background-color: #f9f9fa"><code>
|
||||
{
|
||||
"pages":[
|
||||
{
|
||||
"path": "pages/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "主页"
|
||||
}
|
||||
}
|
||||
],
|
||||
"subPackages": [
|
||||
{
|
||||
"root": "sub-pkg-a",
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/cat",
|
||||
"style": {
|
||||
"navigationBarTitleText": "cat"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/dog",
|
||||
"style": {
|
||||
"navigationBarTitleText": "dog"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "sub-pkg-b",
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/apple",
|
||||
"style": {
|
||||
"navigationBarTitleText": "apple"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}</code></pre>
|
||||
`);
|
||||
onMounted(() => {});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.faq-container {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
.wechat {
|
||||
padding: 30rpx;
|
||||
margin: 20px 0;
|
||||
font-size: 14px;
|
||||
color: var(--wot-card-content-color, rgba(0, 0, 0, 0.45));
|
||||
background-color: #fff;
|
||||
.tips {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
193
src/pages/mine/feedback/index.vue
Normal file
193
src/pages/mine/feedback/index.vue
Normal file
@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<view class="feedback-container">
|
||||
<!-- 问题类型选择 -->
|
||||
<wd-cell-group title="问题类型" border>
|
||||
<view class="radio-group">
|
||||
<label v-for="item in feedbackTypes" :key="item.value" class="radio-item">
|
||||
<text class="radio-text">{{ item.label }}</text>
|
||||
<radio
|
||||
:value="item.value"
|
||||
:checked="feedbackType === item.value"
|
||||
color="#0083ff"
|
||||
class="radio-button"
|
||||
style="transform: scale(0.8)"
|
||||
@click="handleRadioChange(item.value)"
|
||||
/>
|
||||
</label>
|
||||
</view>
|
||||
</wd-cell-group>
|
||||
|
||||
<!-- 问题描述 -->
|
||||
<wd-cell-group title="问题描述" border>
|
||||
<wd-textarea
|
||||
v-model="description"
|
||||
placeholder="请详细描述您遇到的问题或建议..."
|
||||
:maxlength="500"
|
||||
show-count
|
||||
:rows="5"
|
||||
/>
|
||||
</wd-cell-group>
|
||||
|
||||
<!-- 图片上传 -->
|
||||
<wd-cell-group title="相关截图(选填)" border>
|
||||
<view class="upload-box">
|
||||
<wd-upload
|
||||
v-model="fileList"
|
||||
:max-count="3"
|
||||
:before-read="beforeRead"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
</view>
|
||||
</wd-cell-group>
|
||||
|
||||
<!-- 联系方式 -->
|
||||
<wd-cell-group title="联系方式(选填)" border>
|
||||
<wd-input v-model="contact" placeholder="请输入您的手机号或邮箱" clearable />
|
||||
</wd-cell-group>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<view class="submit-btn">
|
||||
<wd-button type="primary" block :loading="submitting" @click="handleSubmit">
|
||||
提交反馈
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<wd-toast />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue";
|
||||
import { useToast } from "wot-design-uni";
|
||||
import { checkLogin } from "@/utils/auth";
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
// 检查登录状态
|
||||
onLoad(() => {
|
||||
if (!checkLogin()) return;
|
||||
});
|
||||
|
||||
// 问题类型选项
|
||||
const feedbackTypes = [
|
||||
{ label: "功能异常", value: "bug" },
|
||||
{ label: "优化建议", value: "suggestion" },
|
||||
{ label: "其他问题", value: "other" },
|
||||
];
|
||||
|
||||
// 表单数据
|
||||
const feedbackType = ref("bug");
|
||||
const description = ref("");
|
||||
const fileList = ref<any[]>([]);
|
||||
const contact = ref("");
|
||||
const submitting = ref(false);
|
||||
|
||||
// 图片上传前的校验
|
||||
const beforeRead = (file: any) => {
|
||||
// 验证文件类型
|
||||
const validTypes = ["image/jpeg", "image/png", "image/gif"];
|
||||
if (!validTypes.includes(file.type)) {
|
||||
toast.error("请上传 jpg、png 或 gif 格式的图片");
|
||||
return false;
|
||||
}
|
||||
// 验证文件大小(限制为 5MB)
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
toast.error("图片大小不能超过 5MB");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// 删除图片
|
||||
const handleDelete = (detail: any) => {
|
||||
const index = detail.index;
|
||||
fileList.value.splice(index, 1);
|
||||
};
|
||||
|
||||
// 处理单选框变化
|
||||
const handleRadioChange = (value: string) => {
|
||||
feedbackType.value = value;
|
||||
};
|
||||
|
||||
// 提交反馈
|
||||
const handleSubmit = async () => {
|
||||
// 表单验证
|
||||
if (!description.value.trim()) {
|
||||
toast.error("请描述您遇到的问题");
|
||||
return;
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
// TODO: 调用提交反馈的接口
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500)); // 模拟提交
|
||||
toast.success("提交成功");
|
||||
// 重置表单
|
||||
description.value = "";
|
||||
fileList.value = [];
|
||||
contact.value = "";
|
||||
|
||||
// 延迟返回上一页
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1500);
|
||||
} catch {
|
||||
toast.error("提交失败,请重试");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.feedback-container {
|
||||
min-height: 100vh;
|
||||
padding: 20rpx 0;
|
||||
background-color: #f5f5f5;
|
||||
|
||||
:deep(.wd-cell-group__title) {
|
||||
padding: 20rpx 30rpx 10rpx;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
padding: 4rpx 0;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.radio-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10rpx 30rpx;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.radio-text {
|
||||
font-size: 22rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.upload-box {
|
||||
padding: 20rpx 30rpx;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
margin: 40rpx 30rpx;
|
||||
}
|
||||
|
||||
:deep(.wd-textarea) {
|
||||
padding: 20rpx 30rpx;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.radio-button {
|
||||
margin-right: -8rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
582
src/pages/mine/index.vue
Normal file
582
src/pages/mine/index.vue
Normal file
@ -0,0 +1,582 @@
|
||||
<template>
|
||||
<view class="mine-container">
|
||||
<!-- 用户信息卡片 -->
|
||||
<view class="user-profile">
|
||||
<view class="blur-bg"></view>
|
||||
<view class="user-info">
|
||||
<view class="avatar-container" @click="navigateToProfile">
|
||||
<image
|
||||
class="avatar"
|
||||
:src="isLogin ? userInfo!.avatar : defaultAvatar"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
</view>
|
||||
<view class="user-details">
|
||||
<block v-if="isLogin">
|
||||
<view class="nickname">{{ userInfo!.nickname || "匿名用户" }}</view>
|
||||
<view class="user-id">ID: {{ userInfo?.username || "0000000" }}</view>
|
||||
</block>
|
||||
<block v-else>
|
||||
<view class="login-prompt">立即登录获取更多功能</view>
|
||||
<wd-button
|
||||
custom-class="btn-login"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="navigateToLoginPage"
|
||||
>
|
||||
登录/注册
|
||||
</wd-button>
|
||||
</block>
|
||||
</view>
|
||||
<view class="actions">
|
||||
<view class="action-btn" @click="navigateToSettings">
|
||||
<wd-icon name="setting1" size="22" color="#333" />
|
||||
</view>
|
||||
<view v-if="isLogin" class="action-btn" @click="navigateToSection('messages')">
|
||||
<wd-icon name="notification" size="22" color="#333" />
|
||||
<view v-if="true" class="badge">2</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- <!– 数据统计 –>-->
|
||||
<!-- <view class="stats-container">-->
|
||||
<!-- <view class="stat-item" @click="navigateToSection('wallet')">-->
|
||||
<!-- <view class="stat-value">0.00</view>-->
|
||||
<!-- <view class="stat-label">我的余额</view>-->
|
||||
<!-- </view>-->
|
||||
<!-- <view class="divider"></view>-->
|
||||
<!-- <view class="stat-item" @click="navigateToSection('favorites')">-->
|
||||
<!-- <view class="stat-value">0</view>-->
|
||||
<!-- <view class="stat-label">我的收藏</view>-->
|
||||
<!-- </view>-->
|
||||
<!-- <view class="divider"></view>-->
|
||||
<!-- <view class="stat-item" @click="navigateToSection('history')">-->
|
||||
<!-- <view class="stat-value">0</view>-->
|
||||
<!-- <view class="stat-label">浏览历史</view>-->
|
||||
<!-- </view>-->
|
||||
<!-- </view>-->
|
||||
|
||||
<!-- 常用工具 -->
|
||||
<view class="card-container">
|
||||
<view class="card-header">
|
||||
<view class="card-title">
|
||||
<wd-icon name="tools" size="18" :color="themeStore.primaryColor" />
|
||||
<text>常用工具</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="tools-grid">
|
||||
<view class="tool-item" @click="navigateToProfile">
|
||||
<view class="tool-icon">
|
||||
<wd-icon name="user" size="24" :color="themeStore.primaryColor" />
|
||||
</view>
|
||||
<view class="tool-label">个人资料</view>
|
||||
</view>
|
||||
|
||||
<view class="tool-item" @click="navigateToFAQ">
|
||||
<view class="tool-icon">
|
||||
<wd-icon name="help-circle" size="24" :color="themeStore.primaryColor" />
|
||||
</view>
|
||||
<view class="tool-label">常见问题</view>
|
||||
</view>
|
||||
<view class="tool-item" @click="handleQuestionFeedback">
|
||||
<view class="tool-icon">
|
||||
<wd-icon name="check-circle" size="24" :color="themeStore.primaryColor" />
|
||||
</view>
|
||||
<view class="tool-label">问题反馈</view>
|
||||
</view>
|
||||
<view class="tool-item" @click="navigateToAbout">
|
||||
<view class="tool-icon">
|
||||
<wd-icon name="info-circle" size="24" :color="themeStore.primaryColor" />
|
||||
</view>
|
||||
<view class="tool-label">关于我们</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- <!– 推荐服务 –>-->
|
||||
<!-- <view class="card-container">-->
|
||||
<!-- <view class="card-header">-->
|
||||
<!-- <view class="card-title">-->
|
||||
<!-- <wd-icon name="star" size="18" :color="themeStore.primaryColor" />-->
|
||||
<!-- <text>推荐服务</text>-->
|
||||
<!-- </view>-->
|
||||
<!-- </view>-->
|
||||
<!-- <view class="services-list">-->
|
||||
<!-- <view class="service-item" @click="navigateToSection('services', 'vip')">-->
|
||||
<!-- <view class="service-left">-->
|
||||
<!-- <view class="service-icon">-->
|
||||
<!-- <wd-icon name="dong" size="22" :color="themeStore.primaryColor" />-->
|
||||
<!-- </view>-->
|
||||
<!-- <view class="service-info">-->
|
||||
<!-- <view class="service-name">会员中心</view>-->
|
||||
<!-- <view class="service-desc">解锁更多特权</view>-->
|
||||
<!-- </view>-->
|
||||
<!-- </view>-->
|
||||
<!-- <wd-icon name="arrow-right" size="14" color="#999" />-->
|
||||
<!-- </view>-->
|
||||
<!-- <view class="service-item" @click="navigateToSection('services', 'coupon')">-->
|
||||
<!-- <view class="service-left">-->
|
||||
<!-- <view class="service-icon">-->
|
||||
<!-- <wd-icon name="discount" size="22" :color="themeStore.primaryColor" />-->
|
||||
<!-- </view>-->
|
||||
<!-- <view class="service-info">-->
|
||||
<!-- <view class="service-name">优惠券</view>-->
|
||||
<!-- <view class="service-desc">查看我的优惠券</view>-->
|
||||
<!-- </view>-->
|
||||
<!-- </view>-->
|
||||
<!-- <wd-icon name="arrow-right" size="14" color="#999" />-->
|
||||
<!-- </view>-->
|
||||
<!-- <view class="service-item" @click="navigateToSection('services', 'invite')">-->
|
||||
<!-- <view class="service-left">-->
|
||||
<!-- <view class="service-icon">-->
|
||||
<!-- <wd-icon name="share" size="22" :color="themeStore.primaryColor" />-->
|
||||
<!-- </view>-->
|
||||
<!-- <view class="service-info">-->
|
||||
<!-- <view class="service-name">邀请有礼</view>-->
|
||||
<!-- <view class="service-desc">邀请好友得奖励</view>-->
|
||||
<!-- </view>-->
|
||||
<!-- </view>-->
|
||||
<!-- <wd-icon name="arrow-right" size="14" color="#999" />-->
|
||||
<!-- </view>-->
|
||||
<!-- </view>-->
|
||||
<!-- </view>-->
|
||||
|
||||
<view v-if="isLogin" class="logout-btn-container">
|
||||
<wd-button custom-class="logout-btn-unocss" @click="handleLogout">退出登录</wd-button>
|
||||
</view>
|
||||
|
||||
<wd-toast />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from "wot-design-uni";
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
import { useThemeStore } from "@/store/modules/theme";
|
||||
import { computed } from "vue";
|
||||
|
||||
const toast = useToast();
|
||||
const userStore = useUserStore();
|
||||
const themeStore = useThemeStore();
|
||||
const userInfo = computed(() => userStore.userInfo);
|
||||
const isLogin = computed(() => !!userInfo.value);
|
||||
const defaultAvatar = "/static/images/default-avatar.png";
|
||||
|
||||
// 登录
|
||||
const navigateToLoginPage = () => {
|
||||
const pages = getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
const currentPagePath = `/${currentPage.route}`;
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages/login/index?redirect=${encodeURIComponent(currentPagePath)}`,
|
||||
});
|
||||
};
|
||||
|
||||
// 退出登录
|
||||
const handleLogout = () => {
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: "确认退出登录吗?",
|
||||
success: function (res) {
|
||||
if (res.confirm) {
|
||||
userStore.logout();
|
||||
toast.show("已退出登录");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 个人信息
|
||||
const navigateToProfile = () => {
|
||||
if (!isLogin.value) {
|
||||
navigateToLoginPage();
|
||||
return;
|
||||
}
|
||||
uni.navigateTo({ url: "/pages/mine/profile/index" });
|
||||
};
|
||||
|
||||
// 常见问题
|
||||
const navigateToFAQ = () => {
|
||||
uni.navigateTo({ url: "/pages/mine/faq/index" });
|
||||
};
|
||||
|
||||
// 关于我们
|
||||
const navigateToAbout = () => {
|
||||
uni.navigateTo({ url: "/pages/mine/about/index" });
|
||||
};
|
||||
|
||||
// 设置
|
||||
const navigateToSettings = () => {
|
||||
uni.navigateTo({ url: "/pages/mine/settings/index" });
|
||||
};
|
||||
|
||||
// 问题反馈
|
||||
const handleQuestionFeedback = () => {
|
||||
uni.navigateTo({ url: "/pages/mine/feedback/index" });
|
||||
};
|
||||
|
||||
// 导航到各个板块
|
||||
const navigateToSection = (section: string, subSection?: string) => {
|
||||
if (!isLogin.value && section !== "services") {
|
||||
navigateToLoginPage();
|
||||
return;
|
||||
}
|
||||
|
||||
const sections: Record<string, string> = {
|
||||
messages: "消息中心",
|
||||
todos: "待办事项",
|
||||
favorites: "我的收藏",
|
||||
history: "浏览历史",
|
||||
wallet: "我的钱包",
|
||||
orders: "我的订单",
|
||||
address: "收货地址",
|
||||
services: "增值服务",
|
||||
};
|
||||
|
||||
let message = sections[section];
|
||||
if (subSection) {
|
||||
message += ` - ${subSection}`;
|
||||
}
|
||||
|
||||
toast.show(`${message}功能开发中...`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* stylelint-disable declaration-property-value-no-unknown */
|
||||
.mine-container {
|
||||
min-height: 100vh;
|
||||
padding-bottom: 100rpx;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
// 用户信息卡片
|
||||
.user-profile {
|
||||
position: relative;
|
||||
padding: 30rpx;
|
||||
overflow: hidden;
|
||||
|
||||
.blur-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
height: 240rpx;
|
||||
background: linear-gradient(to bottom, var(--primary-color), var(--primary-color-light));
|
||||
}
|
||||
|
||||
.user-info {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.avatar-container {
|
||||
position: relative;
|
||||
|
||||
.avatar {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border: 4rpx solid rgba(255, 255, 255, 0.8);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.user-details {
|
||||
flex: 1;
|
||||
margin-left: 24rpx;
|
||||
|
||||
.nickname {
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 34rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.user-id {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.login-prompt {
|
||||
margin-bottom: 16rpx;
|
||||
font-size: 28rpx;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
|
||||
.action-btn {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 70rpx;
|
||||
height: 70rpx;
|
||||
margin-left: 16rpx;
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: -6rpx;
|
||||
right: -6rpx;
|
||||
z-index: 2;
|
||||
min-width: 32rpx;
|
||||
height: 32rpx;
|
||||
padding: 0 6rpx;
|
||||
font-size: 20rpx;
|
||||
line-height: 32rpx;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
background-color: #ff4d4f;
|
||||
border: 2rpx solid #fff;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 数据统计
|
||||
.stats-container {
|
||||
display: flex;
|
||||
padding: 30rpx 20rpx;
|
||||
margin: 20rpx 30rpx;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.stat-value {
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 1px;
|
||||
margin: 0 20rpx;
|
||||
background-color: #eee;
|
||||
}
|
||||
}
|
||||
|
||||
// 卡片容器通用样式
|
||||
.card-container {
|
||||
margin: 24rpx 30rpx;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 24rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
|
||||
.card-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
text {
|
||||
margin-left: 12rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.card-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
text {
|
||||
margin-right: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 订单状态
|
||||
.order-status {
|
||||
display: flex;
|
||||
padding: 30rpx 0 20rpx;
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.status-icon {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
.status-badge {
|
||||
position: absolute;
|
||||
top: -10rpx;
|
||||
right: -10rpx;
|
||||
z-index: 2;
|
||||
min-width: 32rpx;
|
||||
height: 32rpx;
|
||||
padding: 0 6rpx;
|
||||
font-size: 20rpx;
|
||||
line-height: 32rpx;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
background-color: #ff4d4f;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 工具网格
|
||||
.tools-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 20rpx 0 10rpx;
|
||||
|
||||
.tool-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 25%;
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.tool-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 90rpx;
|
||||
height: 90rpx;
|
||||
margin-bottom: 12rpx;
|
||||
background-color: rgba(var(--primary-color-rgb), 0.08);
|
||||
border-radius: 18rpx;
|
||||
transition: transform 0.2s;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.tool-label {
|
||||
font-size: 24rpx;
|
||||
color: #555;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 服务列表
|
||||
.services-list {
|
||||
.service-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 30rpx 24rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.service-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.service-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 70rpx;
|
||||
height: 70rpx;
|
||||
background-color: rgba(var(--primary-color-rgb), 0.08);
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.service-info {
|
||||
margin-left: 20rpx;
|
||||
|
||||
.service-name {
|
||||
margin-bottom: 6rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.service-desc {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 退出登录按钮
|
||||
.logout-btn-container {
|
||||
margin: 60rpx 30rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* 使用全局样式解决微信小程序组件样式问题 */
|
||||
.logout-btn-unocss {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
width: 100% !important;
|
||||
height: 80rpx !important;
|
||||
font-size: 32rpx !important;
|
||||
font-weight: bold !important;
|
||||
color: #fff !important;
|
||||
background-color: var(--primary-color) !important;
|
||||
border: 2rpx solid var(--primary-color) !important;
|
||||
border-radius: 40rpx !important;
|
||||
box-shadow: 0 4rpx 12rpx rgba(var(--primary-color-rgb), 0.3) !important;
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
width: 160rpx !important;
|
||||
height: 60rpx !important;
|
||||
font-size: 26rpx !important;
|
||||
color: var(--primary-color) !important;
|
||||
background-color: #fff !important;
|
||||
border: none !important;
|
||||
border-radius: 30rpx !important;
|
||||
}
|
||||
</style>
|
220
src/pages/mine/profile/index.vue
Normal file
220
src/pages/mine/profile/index.vue
Normal file
@ -0,0 +1,220 @@
|
||||
<template>
|
||||
<view class="profile">
|
||||
<view v-if="userProfile" class="profile-card">
|
||||
<wd-cell-group border>
|
||||
<wd-cell class="avatar-cell" title="头像" center is-link>
|
||||
<view class="avatar">
|
||||
<view v-if="!userProfile.avatar" class="img" @click="avatarUpload">
|
||||
<wd-icon name="fill-camera" custom-class="img-icon" />
|
||||
</view>
|
||||
<wd-img
|
||||
v-if="userProfile.avatar"
|
||||
round
|
||||
width="80px"
|
||||
height="80px"
|
||||
:src="userProfile.avatar"
|
||||
mode="aspectFit"
|
||||
custom-class="profile-img"
|
||||
@click="avatarUpload"
|
||||
/>
|
||||
</view>
|
||||
</wd-cell>
|
||||
<wd-cell title="昵称" :value="userProfile.nickname" is-link @click="handleOpenDialog()" />
|
||||
<wd-cell
|
||||
title="性别"
|
||||
:value="userProfile.gender === 1 ? '男' : userProfile.gender === 2 ? '女' : '未知'"
|
||||
is-link
|
||||
@click="handleOpenDialog()"
|
||||
/>
|
||||
<wd-cell title="用户名" :value="userProfile.username" />
|
||||
<wd-cell title="部门" :value="userProfile.deptName" />
|
||||
<wd-cell title="角色" :value="userProfile.roleNames" />
|
||||
<wd-cell title="创建日期" :value="userProfile.createTime" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!--头像裁剪-->
|
||||
<wd-img-cropper v-model="avatarShow" :img-src="originalSrc" @confirm="handleAvatarConfirm" />
|
||||
|
||||
<!--用户信息编辑弹出框-->
|
||||
<wd-popup v-model="dialog.visible" position="bottom">
|
||||
<wd-form ref="userProfileFormRef" :model="userProfileForm" custom-class="edit-form">
|
||||
<wd-cell-group border>
|
||||
<wd-input
|
||||
v-model="userProfileForm.nickname"
|
||||
label="昵称"
|
||||
label-width="160rpx"
|
||||
placeholder="请输入昵称"
|
||||
prop="nickname"
|
||||
:rules="rules.nickname"
|
||||
/>
|
||||
<wd-cell title="性别" title-width="160rpx" center prop="gender" :rules="rules.gender">
|
||||
<wd-radio-group v-model="userProfileForm.gender" shape="button" class="ef-radio-group">
|
||||
<wd-radio :value="1">男</wd-radio>
|
||||
<wd-radio :value="2">女</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
<view class="footer">
|
||||
<wd-button type="primary" size="large" block @click="handleSubmit">提交</wd-button>
|
||||
</view>
|
||||
</wd-form>
|
||||
</wd-popup>
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import UserAPI, { type UserProfileVO, UserProfileForm } from "@/api/system/user";
|
||||
import FileAPI, { type FileInfo } from "@/api/file";
|
||||
import { checkLogin } from "@/utils/auth";
|
||||
|
||||
const originalSrc = ref<string>(""); //选取的原图路径
|
||||
const avatarShow = ref<boolean>(false); //显示头像裁剪
|
||||
const userProfile = ref<UserProfileVO>(); //用户信息
|
||||
|
||||
/** 加载用户信息 */
|
||||
const loadUserProfile = async () => {
|
||||
userProfile.value = await UserAPI.getProfile();
|
||||
};
|
||||
|
||||
// 头像选择
|
||||
function avatarUpload() {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
success: (res) => {
|
||||
originalSrc.value = res.tempFilePaths[0];
|
||||
avatarShow.value = true;
|
||||
},
|
||||
});
|
||||
}
|
||||
// 头像裁剪完成
|
||||
function handleAvatarConfirm(event: any) {
|
||||
const { tempFilePath } = event;
|
||||
FileAPI.upload(tempFilePath).then((fileInfo: FileInfo) => {
|
||||
const avatarForm: UserProfileForm = {
|
||||
avatar: fileInfo.url,
|
||||
};
|
||||
// 头像路径保存至后端
|
||||
UserAPI.updateProfile(avatarForm).then(() => {
|
||||
uni.showToast({ title: "头像上传成功", icon: "none" });
|
||||
loadUserProfile();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 本页面中所有的校验规则
|
||||
const rules = reactive({
|
||||
nickname: [{ required: true, message: "请填写昵称" }],
|
||||
gender: [{ required: true, message: "请选择性别" }],
|
||||
});
|
||||
|
||||
const dialog = reactive({
|
||||
visible: false,
|
||||
});
|
||||
|
||||
const userProfileForm = reactive<UserProfileForm>({});
|
||||
const userProfileFormRef = ref();
|
||||
|
||||
/**
|
||||
* 打开弹窗
|
||||
* @param type 弹窗类型 ACCOUNT: 账号资料 PASSWORD: 修改密码 MOBILE: 绑定手机 EMAIL: 绑定邮箱
|
||||
*/
|
||||
const handleOpenDialog = () => {
|
||||
dialog.visible = true;
|
||||
// 初始化表单数据
|
||||
userProfileForm.nickname = userProfile.value?.nickname;
|
||||
userProfileForm.gender = userProfile.value?.gender;
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
function handleSubmit() {
|
||||
userProfileFormRef.value.validate().then(({ valid }: { valid: boolean }) => {
|
||||
if (valid) {
|
||||
UserAPI.updateProfile(userProfileForm).then(() => {
|
||||
uni.showToast({ title: "账号资料修改成功", icon: "none" });
|
||||
dialog.visible = false;
|
||||
loadUserProfile();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 检查登录状态
|
||||
onLoad(() => {
|
||||
if (!checkLogin()) return;
|
||||
|
||||
// #ifdef H5
|
||||
document.addEventListener("touchstart", touchstartListener, { passive: false });
|
||||
document.addEventListener("touchmove", touchmoveListener, { passive: false });
|
||||
// #endif
|
||||
loadUserProfile();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 在onMounted中不再重复检查登录状态和加载用户信息
|
||||
// 如果需要检查登录状态和加载用户信息,请使用onLoad中的逻辑
|
||||
});
|
||||
|
||||
// 页面销毁前移除事件监听
|
||||
onBeforeUnmount(() => {
|
||||
// #ifdef H5
|
||||
document.removeEventListener("touchstart", touchstartListener);
|
||||
document.removeEventListener("touchmove", touchmoveListener);
|
||||
// #endif
|
||||
});
|
||||
// 禁用浏览器双指缩放,使头像裁剪时双指缩放能够起作用
|
||||
function touchstartListener(event: TouchEvent) {
|
||||
if (event.touches.length > 1) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
// 禁用浏览器下拉刷新,使头像裁剪时能够移动图片
|
||||
function touchmoveListener(event: TouchEvent) {
|
||||
event.preventDefault();
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.profile {
|
||||
.profile-card {
|
||||
padding: 10rpx;
|
||||
margin-bottom: 24rpx;
|
||||
line-height: 1.1;
|
||||
background-color: rgb(255, 255, 255);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.05);
|
||||
|
||||
.avatar-cell {
|
||||
:deep(.wd-cell__body) {
|
||||
align-items: center;
|
||||
}
|
||||
.avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: right;
|
||||
.img {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
border-radius: 50%;
|
||||
.img-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.edit-form {
|
||||
padding-top: 40rpx;
|
||||
.ef-radio-group {
|
||||
line-height: 1;
|
||||
text-align: left;
|
||||
}
|
||||
.footer {
|
||||
padding: 24rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
343
src/pages/mine/settings/account/index.vue
Normal file
343
src/pages/mine/settings/account/index.vue
Normal file
@ -0,0 +1,343 @@
|
||||
<template>
|
||||
<view class="profile">
|
||||
<view class="profile-card">
|
||||
<wd-cell-group border>
|
||||
<wd-cell
|
||||
title="账户密码"
|
||||
label="定期修改密码有助于保护账户安全"
|
||||
value="修改"
|
||||
is-link
|
||||
@click="handleOpenDialog(DialogType.PASSWORD)"
|
||||
/>
|
||||
<wd-cell
|
||||
title="绑定手机"
|
||||
:value="userProfile?.mobile"
|
||||
is-link
|
||||
@click="handleOpenDialog(DialogType.MOBILE)"
|
||||
/>
|
||||
<wd-cell
|
||||
title="绑定邮箱"
|
||||
:value="userProfile?.email ? userProfile.email : '未绑定邮箱'"
|
||||
is-link
|
||||
@click="handleOpenDialog(DialogType.EMAIL)"
|
||||
/>
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!--用户信息编辑弹出框-->
|
||||
<wd-popup v-model="dialog.visible" position="bottom">
|
||||
<wd-form
|
||||
v-if="dialog.type === DialogType.PASSWORD"
|
||||
ref="passwordChangeFormRef"
|
||||
:model="passwordChangeForm"
|
||||
custom-class="edit-form"
|
||||
>
|
||||
<wd-cell-group border>
|
||||
<wd-input
|
||||
v-model="passwordChangeForm.oldPassword"
|
||||
label="原密码"
|
||||
label-width="160rpx"
|
||||
show-password
|
||||
clearable
|
||||
placeholder="请输入原密码"
|
||||
prop="oldPassword"
|
||||
:rules="rules.oldPassword"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="passwordChangeForm.newPassword"
|
||||
label="新密码"
|
||||
label-width="160rpx"
|
||||
show-password
|
||||
clearable
|
||||
placeholder="请输入新密码"
|
||||
prop="newPassword"
|
||||
:rules="rules.newPassword"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="passwordChangeForm.confirmPassword"
|
||||
label="确认密码"
|
||||
label-width="160rpx"
|
||||
show-password
|
||||
clearable
|
||||
placeholder="请确认新密码"
|
||||
prop="confirmPassword"
|
||||
:rules="rules.confirmPassword"
|
||||
/>
|
||||
</wd-cell-group>
|
||||
<view class="footer">
|
||||
<wd-button type="primary" size="large" block @click="handleSubmit">提交</wd-button>
|
||||
</view>
|
||||
</wd-form>
|
||||
<wd-form
|
||||
v-if="dialog.type === DialogType.MOBILE"
|
||||
ref="mobileBindingFormRef"
|
||||
:model="mobileBindingForm"
|
||||
custom-class="edit-form"
|
||||
>
|
||||
<wd-cell-group border>
|
||||
<wd-input
|
||||
v-model="mobileBindingForm.mobile"
|
||||
label="手机号码"
|
||||
label-width="160rpx"
|
||||
clearable
|
||||
placeholder="请输入手机号码"
|
||||
prop="mobile"
|
||||
:rules="rules.mobile"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="mobileBindingForm.code"
|
||||
label="验证码"
|
||||
label-width="160rpx"
|
||||
clearable
|
||||
placeholder="请输入验证码"
|
||||
prop="code"
|
||||
:rules="rules.code"
|
||||
>
|
||||
<template #suffix>
|
||||
<wd-button
|
||||
plain
|
||||
:disabled="mobileCountdown > 0"
|
||||
@click="handleSendVerificationCode('MOBILE')"
|
||||
>
|
||||
{{ mobileCountdown > 0 ? `${mobileCountdown}s后重新发送` : "发送验证码" }}
|
||||
</wd-button>
|
||||
</template>
|
||||
</wd-input>
|
||||
</wd-cell-group>
|
||||
<view class="footer">
|
||||
<wd-button type="primary" size="large" block @click="handleSubmit">提交</wd-button>
|
||||
</view>
|
||||
</wd-form>
|
||||
<wd-form
|
||||
v-if="dialog.type === DialogType.EMAIL"
|
||||
ref="emailBindingFormRef"
|
||||
:model="emailBindingForm"
|
||||
custom-class="edit-form"
|
||||
>
|
||||
<wd-cell-group border>
|
||||
<wd-input
|
||||
v-model="emailBindingForm.email"
|
||||
label="邮箱"
|
||||
label-width="160rpx"
|
||||
clearable
|
||||
placeholder="请输入邮箱"
|
||||
prop="email"
|
||||
:rules="rules.email"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="emailBindingForm.code"
|
||||
label="验证码"
|
||||
label-width="160rpx"
|
||||
clearable
|
||||
placeholder="请输入验证码"
|
||||
prop="code"
|
||||
:rules="rules.code"
|
||||
>
|
||||
<template #suffix>
|
||||
<wd-button
|
||||
plain
|
||||
:disabled="emailCountdown > 0"
|
||||
@click="handleSendVerificationCode('EMAIL')"
|
||||
>
|
||||
{{ emailCountdown > 0 ? `${emailCountdown}s后重新发送` : "发送验证码" }}
|
||||
</wd-button>
|
||||
</template>
|
||||
</wd-input>
|
||||
</wd-cell-group>
|
||||
<view class="footer">
|
||||
<wd-button type="primary" size="large" block @click="handleSubmit">提交</wd-button>
|
||||
</view>
|
||||
</wd-form>
|
||||
</wd-popup>
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import UserAPI, {
|
||||
PasswordChangeForm,
|
||||
MobileBindingForm,
|
||||
EmailBindingForm,
|
||||
UserProfileVO,
|
||||
} from "@/api/system/user";
|
||||
|
||||
const validatorConfirmPassword = (value: string) => {
|
||||
if (!value) {
|
||||
return Promise.reject("请确认密码");
|
||||
} else {
|
||||
if (value !== passwordChangeForm.newPassword) {
|
||||
return Promise.reject("两次输入的密码不一致");
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
};
|
||||
// 本页面中所有的校验规则
|
||||
const rules = reactive({
|
||||
oldPassword: [{ required: true, message: "请填写原密码" }],
|
||||
newPassword: [{ required: true, message: "请填写新密码" }],
|
||||
confirmPassword: [{ required: true, message: "请确认密码", validator: validatorConfirmPassword }],
|
||||
mobile: [{ required: true, pattern: /^1[3-9]\d{9}$/, message: "请填写正确的手机号码" }],
|
||||
code: [{ required: true, message: "请填写验证码" }],
|
||||
email: [
|
||||
{
|
||||
required: true,
|
||||
pattern: /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/,
|
||||
message: "请填写正确的邮箱地址",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
enum DialogType {
|
||||
PASSWORD = "password",
|
||||
MOBILE = "mobile",
|
||||
EMAIL = "email",
|
||||
}
|
||||
|
||||
const dialog = reactive({
|
||||
visible: false,
|
||||
type: "" as DialogType, // 修改账号资料,修改密码、绑定手机、绑定邮箱
|
||||
});
|
||||
|
||||
const userProfile = ref<UserProfileVO>(); //用户信息
|
||||
const passwordChangeForm = reactive<PasswordChangeForm>({});
|
||||
const mobileBindingForm = reactive<MobileBindingForm>({});
|
||||
const emailBindingForm = reactive<EmailBindingForm>({});
|
||||
const passwordChangeFormRef = ref();
|
||||
const mobileBindingFormRef = ref();
|
||||
const emailBindingFormRef = ref();
|
||||
|
||||
const mobileCountdown = ref(0);
|
||||
const mobileTimer = ref<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
const emailCountdown = ref(0);
|
||||
const emailTimer = ref<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
/** 加载用户信息 */
|
||||
const loadUserProfile = async () => {
|
||||
userProfile.value = await UserAPI.getProfile();
|
||||
};
|
||||
|
||||
/**
|
||||
* 打开弹窗
|
||||
* @param type 弹窗类型 ACCOUNT: 账号资料 PASSWORD: 修改密码 MOBILE: 绑定手机 EMAIL: 绑定邮箱
|
||||
*/
|
||||
const handleOpenDialog = (type: DialogType) => {
|
||||
dialog.type = type;
|
||||
dialog.visible = true;
|
||||
switch (type) {
|
||||
case DialogType.PASSWORD:
|
||||
passwordChangeForm.oldPassword = "";
|
||||
passwordChangeForm.newPassword = "";
|
||||
passwordChangeForm.confirmPassword = "";
|
||||
break;
|
||||
case DialogType.MOBILE:
|
||||
mobileBindingForm.mobile = "";
|
||||
mobileBindingForm.code = "";
|
||||
break;
|
||||
case DialogType.EMAIL:
|
||||
emailBindingForm.email = "";
|
||||
emailBindingForm.code = "";
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 发送验证码
|
||||
*
|
||||
* @param contactType 联系方式类型 MOBILE: 手机号码 EMAIL: 邮箱
|
||||
*/
|
||||
const handleSendVerificationCode = async (contactType: string) => {
|
||||
if (contactType === "MOBILE") {
|
||||
mobileBindingFormRef.value.validate("mobile").then(({ valid }: { valid: boolean }) => {
|
||||
if (valid) {
|
||||
UserAPI.sendVerificationCode(mobileBindingForm.mobile!, "MOBILE").then(() => {
|
||||
uni.showToast({ title: "验证码已发送", icon: "none" });
|
||||
mobileCountdown.value = 60;
|
||||
mobileTimer.value = setInterval(() => {
|
||||
if (mobileCountdown.value > 0) {
|
||||
mobileCountdown.value -= 1;
|
||||
} else {
|
||||
clearInterval(mobileTimer.value!);
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (contactType === "EMAIL") {
|
||||
emailBindingFormRef.value.validate("email").then(({ valid }: { valid: boolean }) => {
|
||||
if (valid) {
|
||||
UserAPI.sendVerificationCode(emailBindingForm.email!, "EMAIL").then(() => {
|
||||
uni.showToast({ title: "验证码已发送", icon: "none" });
|
||||
emailCountdown.value = 60;
|
||||
emailTimer.value = setInterval(() => {
|
||||
if (emailCountdown.value > 0) {
|
||||
emailCountdown.value -= 1;
|
||||
} else {
|
||||
clearInterval(emailTimer.value!);
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
function handleSubmit() {
|
||||
if (dialog.type === DialogType.PASSWORD) {
|
||||
passwordChangeFormRef.value.validate().then(({ valid }: { valid: boolean }) => {
|
||||
if (valid) {
|
||||
UserAPI.changePassword(passwordChangeForm).then(() => {
|
||||
uni.showToast({ title: "密码修改成功", icon: "none" });
|
||||
dialog.visible = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (dialog.type === DialogType.MOBILE) {
|
||||
mobileBindingFormRef.value.validate().then(({ valid }: { valid: boolean }) => {
|
||||
if (valid) {
|
||||
UserAPI.bindMobile(mobileBindingForm).then(() => {
|
||||
uni.showToast({ title: "手机号绑定成功", icon: "none" });
|
||||
dialog.visible = false;
|
||||
loadUserProfile();
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (dialog.type === DialogType.EMAIL) {
|
||||
emailBindingFormRef.value.validate().then(({ valid }: { valid: boolean }) => {
|
||||
if (valid) {
|
||||
UserAPI.bindEmail(emailBindingForm).then(() => {
|
||||
uni.showToast({ title: "邮箱绑定成功", icon: "none" });
|
||||
dialog.visible = false;
|
||||
loadUserProfile();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadUserProfile();
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.profile {
|
||||
.profile-card {
|
||||
padding: 10rpx;
|
||||
margin-bottom: 24rpx;
|
||||
line-height: 1.1;
|
||||
background-color: rgb(255, 255, 255);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.edit-form {
|
||||
padding-top: 40rpx;
|
||||
.ef-radio-group {
|
||||
line-height: 1;
|
||||
text-align: left;
|
||||
}
|
||||
.footer {
|
||||
padding: 24rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
98
src/pages/mine/settings/agreement/index.vue
Normal file
98
src/pages/mine/settings/agreement/index.vue
Normal file
@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<view class="agreement-container">
|
||||
<view class="agreement-header">
|
||||
<text class="header-title">用户协议</text>
|
||||
<text class="header-date">更新日期:2024年3月15日</text>
|
||||
</view>
|
||||
|
||||
<view class="agreement-content">
|
||||
<view v-for="(section, index) in agreementContent" :key="index" class="section">
|
||||
<text class="section-title">{{ section.title }}</text>
|
||||
<text class="section-text">{{ section.content }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const agreementContent = [
|
||||
{
|
||||
title: "1. 协议的范围",
|
||||
content:
|
||||
"本协议是您与我们之间关于使用本应用服务所订立的协议。您在使用本应用服务时,须完全接受本协议所有条款。",
|
||||
},
|
||||
{
|
||||
title: "2. 服务内容",
|
||||
content:
|
||||
"本应用向您提供以下服务:网络状态检测、网络性能测试以及其他相关服务。我们将持续优化和更新服务内容,为您提供更好的使用体验。",
|
||||
},
|
||||
{
|
||||
title: "3. 用户隐私",
|
||||
content:
|
||||
"我们重视用户的隐私保护,收集信息仅用于提供网络测试服务、改善用户体验和必要的系统维护。我们承诺对您的信息进行严格保密。",
|
||||
},
|
||||
{
|
||||
title: "4. 用户行为规范",
|
||||
content:
|
||||
"您在使用本服务时必须遵守中华人民共和国相关法律法规。您不得利用本服务从事违法违规活动。如发现违规行为,我们有权终止服务。",
|
||||
},
|
||||
{
|
||||
title: "5. 免责声明",
|
||||
content:
|
||||
"由于网络服务的特殊性,本应用不保证服务一定能满足用户的所有要求。对于因网络状态、通信线路等不可控因素导致的服务中断或其他缺陷,本应用不承担任何责任。",
|
||||
},
|
||||
{
|
||||
title: "6. 协议修改",
|
||||
content:
|
||||
"我们保留随时修改本协议的权利。协议修改后,如果您继续使用本应用服务,即视为您已接受修改后的协议。我们建议您定期查看本协议以了解任何变更。",
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.agreement-container {
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.agreement-header {
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
|
||||
.header-title {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.header-date {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.agreement-content {
|
||||
.section {
|
||||
margin-bottom: 25px;
|
||||
|
||||
.section-title {
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
font-size: 17px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.section-text {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
color: #666;
|
||||
text-align: justify;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
271
src/pages/mine/settings/index.vue
Normal file
271
src/pages/mine/settings/index.vue
Normal file
@ -0,0 +1,271 @@
|
||||
<template>
|
||||
<view class="settings-container">
|
||||
<wd-cell-group>
|
||||
<wd-cell title="个人资料" icon="user" is-link @click="navigateToProfile" />
|
||||
<wd-cell title="账号和安全" icon="secured" is-link @click="navigateToAccount" />
|
||||
<wd-cell title="主题设置" icon="setting1" is-link @click="navigateToTheme" />
|
||||
<wd-cell title="用户协议" icon="user" is-link @click="navigateToUserAgreement" />
|
||||
<wd-cell title="隐私政策" icon="folder" is-link @click="navigateToPrivacy" />
|
||||
<wd-cell title="关于我们" icon="info" is-link @click="navigateToAbout" />
|
||||
</wd-cell-group>
|
||||
|
||||
<wd-cell-group custom-style="margin-top:40rpx">
|
||||
<wd-cell title="网络测试" icon="wifi" is-link @click="navigateToNetworkTest" />
|
||||
<wd-cell
|
||||
title="清空缓存"
|
||||
icon="delete1"
|
||||
:value="cacheSize"
|
||||
clickable
|
||||
@click="handleClearCache"
|
||||
/>
|
||||
</wd-cell-group>
|
||||
|
||||
<view v-if="isLogin" class="logout-section">
|
||||
<wd-button class="logout-btn" @click="handleLogout">退出登录</wd-button>
|
||||
</view>
|
||||
|
||||
<!-- 全屏 loading -->
|
||||
<view v-if="clearing" class="loading-mask">
|
||||
<view class="loading-content">
|
||||
<view class="loading-icon" />
|
||||
<text class="loading-text">正在清理...</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
import { checkLogin } from "@/utils/auth";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
const userStore = useUserStore();
|
||||
const isLogin = computed(() => !!userStore.userInfo);
|
||||
|
||||
// 个人资料
|
||||
const navigateToProfile = () => {
|
||||
if (checkLogin()) {
|
||||
uni.navigateTo({
|
||||
url: "/pages/mine/profile/index",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 账号和安全
|
||||
const navigateToAccount = () => {
|
||||
if (checkLogin()) {
|
||||
uni.navigateTo({
|
||||
url: "/pages/mine/settings/account/index",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 主题设置
|
||||
const navigateToTheme = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/mine/settings/theme/index",
|
||||
});
|
||||
};
|
||||
|
||||
// 用户协议
|
||||
const navigateToUserAgreement = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/mine/agreements/user-agreement",
|
||||
});
|
||||
};
|
||||
|
||||
// 隐私政策
|
||||
const navigateToPrivacy = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/mine/agreements/privacy-policy",
|
||||
});
|
||||
};
|
||||
|
||||
// 关于我们
|
||||
const navigateToAbout = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/mine/about/index",
|
||||
});
|
||||
};
|
||||
|
||||
// 网络测试
|
||||
const navigateToNetworkTest = () => {
|
||||
uni.navigateTo({ url: "/pages/mine/settings/network/index" });
|
||||
};
|
||||
|
||||
// 是否正在清理
|
||||
const clearing = ref(false);
|
||||
// 缓存大小
|
||||
const cacheSize = ref<any>("计算中...");
|
||||
// 获取缓存大小
|
||||
const getCacheSize = async () => {
|
||||
try {
|
||||
// #ifdef MP-WEIXIN
|
||||
const res = await uni.getStorageInfo();
|
||||
cacheSize.value = formatSize(res.currentSize);
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
cacheSize.value = formatSize(
|
||||
Object.keys(localStorage).reduce((size, key) => size + localStorage[key].length, 0)
|
||||
);
|
||||
// #endif
|
||||
if (!cacheSize.value) {
|
||||
cacheSize.value = "0B";
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取缓存大小失败:", error);
|
||||
cacheSize.value = "获取失败";
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化存储大小
|
||||
const formatSize = (size: number) => {
|
||||
if (size < 1024) {
|
||||
return size + "B";
|
||||
} else if (size < 1024 * 1024) {
|
||||
return (size / 1024).toFixed(2) + "KB";
|
||||
} else {
|
||||
return (size / 1024 / 1024).toFixed(2) + "MB";
|
||||
}
|
||||
};
|
||||
|
||||
// 处理清除缓存
|
||||
const handleClearCache = async () => {
|
||||
if (cacheSize.value === "获取失败") {
|
||||
uni.showToast({
|
||||
title: "获取缓存信息失败,请稍后重试",
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (cacheSize.value === "0B") {
|
||||
uni.showToast({
|
||||
title: "暂无缓存需要清理",
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (clearing.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
clearing.value = true;
|
||||
// 模拟清理过程
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
// 清除缓存
|
||||
await uni.clearStorage();
|
||||
// 更新缓存大小显示
|
||||
await getCacheSize();
|
||||
// 提示清理成功
|
||||
uni.showToast({
|
||||
title: "清理成功",
|
||||
icon: "success",
|
||||
});
|
||||
} catch {
|
||||
uni.showToast({
|
||||
title: "清理失败",
|
||||
icon: "error",
|
||||
});
|
||||
} finally {
|
||||
clearing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 退出登录
|
||||
const handleLogout = () => {
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: "确定要退出登录吗?",
|
||||
success: function (res) {
|
||||
if (res.confirm) {
|
||||
userStore.logout();
|
||||
uni.showToast({
|
||||
title: "已退出登录",
|
||||
icon: "success",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 检查登录状态
|
||||
onLoad(() => {
|
||||
if (!checkLogin()) return;
|
||||
|
||||
getCacheSize();
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.settings-container {
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
|
||||
.loading-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
|
||||
.loading-content {
|
||||
padding: 30rpx 40rpx;
|
||||
text-align: center;
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
|
||||
|
||||
.loading-icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
margin: 0 auto 20rpx;
|
||||
border: 4rpx solid #f3f3f3;
|
||||
border-top: 4rpx solid #409eff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logout-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 20rpx;
|
||||
margin-top: 60rpx;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 90rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
background-color: var(--primary-color);
|
||||
border: none;
|
||||
border-radius: 45rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(var(--primary-color-rgb), 0.3);
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:active {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
326
src/pages/mine/settings/network/index.vue
Normal file
326
src/pages/mine/settings/network/index.vue
Normal file
@ -0,0 +1,326 @@
|
||||
<template>
|
||||
<view class="network-container">
|
||||
<!-- 网络状态展示 -->
|
||||
<view class="status-card">
|
||||
<view class="status-header">
|
||||
<text class="title">网络状态</text>
|
||||
<text :class="['status-badge', networkType ? 'online' : 'offline']">
|
||||
{{ networkType ? "在线" : "离线" }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="status-info">
|
||||
<view class="info-item">
|
||||
<text class="label">网络类型:</text>
|
||||
<text class="value">{{ networkType || "未知" }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">网络强度:</text>
|
||||
<text class="value">{{ signalStrength }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 网络测试 -->
|
||||
<view class="test-card">
|
||||
<view class="test-header">
|
||||
<text class="title">网络测试</text>
|
||||
<text class="subtitle">测试服务器连接情况</text>
|
||||
</view>
|
||||
|
||||
<view class="test-content">
|
||||
<view class="test-item">
|
||||
<text class="label">延迟:</text>
|
||||
<text class="value">{{ pingResult.delay }}ms</text>
|
||||
<text :class="['status', getPingStatus]">{{ pingResult.status }}</text>
|
||||
</view>
|
||||
|
||||
<view v-if="testing" class="progress-bar">
|
||||
<view class="progress" :style="{ width: `${progress}%` }" />
|
||||
</view>
|
||||
|
||||
<button class="test-btn" :disabled="testing" @click="startTest">
|
||||
{{ testing ? "测试中..." : "开始测试" }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import request from "@/utils/request";
|
||||
|
||||
interface PingResult {
|
||||
delay: string | number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
// 声明全局 wx 对象
|
||||
declare const wx: any;
|
||||
|
||||
// 响应式状态
|
||||
const networkType = ref("");
|
||||
const signalStrength = ref("获取中...");
|
||||
const testing = ref(false);
|
||||
const progress = ref(0);
|
||||
const pingResult = ref<PingResult>({
|
||||
delay: "--",
|
||||
status: "未测试",
|
||||
});
|
||||
const networkListener = ref<any>(null);
|
||||
|
||||
// 计算属性
|
||||
const getPingStatus = computed(() => {
|
||||
if (pingResult.value.delay === "--") return "";
|
||||
// 将 delay 转换为数字进行比较
|
||||
const delay = Number(pingResult.value.delay);
|
||||
if (isNaN(delay)) return "";
|
||||
if (delay < 100) return "good";
|
||||
if (delay < 300) return "normal";
|
||||
return "bad";
|
||||
});
|
||||
|
||||
// 方法
|
||||
const getNetworkType = async () => {
|
||||
try {
|
||||
const res = await uni.getNetworkType();
|
||||
networkType.value = res.networkType;
|
||||
|
||||
// 微信小程序支持获取信号强度
|
||||
// #ifdef MP-WEIXIN
|
||||
if (wx?.getNetworkWeakness) {
|
||||
const weaknessRes = await wx.getNetworkWeakness();
|
||||
signalStrength.value = `${weaknessRes.weaknessLevel}%`;
|
||||
} else {
|
||||
signalStrength.value = "不支持";
|
||||
}
|
||||
// #endif
|
||||
|
||||
// H5环境
|
||||
// #ifdef H5
|
||||
signalStrength.value = (navigator as any).connection
|
||||
? `${(navigator as any).connection.effectiveType || "未知"}`
|
||||
: "不支持";
|
||||
// #endif
|
||||
} catch {
|
||||
networkType.value = "获取失败";
|
||||
signalStrength.value = "获取失败";
|
||||
}
|
||||
};
|
||||
|
||||
// 监听网络状态变化
|
||||
const listenNetworkStatus = () => {
|
||||
// #ifdef MP-WEIXIN
|
||||
networkListener.value = wx?.onNetworkStatusChange((res: any) => {
|
||||
networkType.value = res.networkType;
|
||||
getNetworkType();
|
||||
});
|
||||
// #endif
|
||||
|
||||
// #ifdef H5
|
||||
window.addEventListener("online", getNetworkType);
|
||||
window.addEventListener("offline", () => {
|
||||
networkType.value = "";
|
||||
signalStrength.value = "离线";
|
||||
});
|
||||
// #endif
|
||||
};
|
||||
|
||||
// 开始网络测试
|
||||
const startTest = async () => {
|
||||
if (testing.value) return;
|
||||
|
||||
testing.value = true;
|
||||
progress.value = 0;
|
||||
pingResult.value.delay = "--";
|
||||
pingResult.value.status = "测试中";
|
||||
|
||||
const progressTimer = setInterval(() => {
|
||||
if (progress.value < 90) {
|
||||
progress.value += 10;
|
||||
}
|
||||
}, 200);
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
// #ifdef H5
|
||||
await uni.request({
|
||||
url: "/api/v1/auth/captcha",
|
||||
timeout: 5000,
|
||||
});
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
await request({
|
||||
url: "/api/v1/auth/captcha",
|
||||
timeout: 5000,
|
||||
});
|
||||
// #endif
|
||||
const endTime = Date.now();
|
||||
const delay = endTime - startTime;
|
||||
|
||||
pingResult.value.delay = delay;
|
||||
pingResult.value.status = delay < 300 ? "正常" : "较慢";
|
||||
} catch {
|
||||
pingResult.value.delay = "--";
|
||||
pingResult.value.status = "连接失败";
|
||||
} finally {
|
||||
clearInterval(progressTimer);
|
||||
progress.value = 100;
|
||||
setTimeout(() => {
|
||||
testing.value = false;
|
||||
progress.value = 0;
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
getNetworkType();
|
||||
listenNetworkStatus();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// #ifdef MP-WEIXIN
|
||||
if (networkListener.value?.clear) {
|
||||
networkListener.value.clear();
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef H5
|
||||
window.removeEventListener("online", getNetworkType);
|
||||
window.removeEventListener("offline", getNetworkType);
|
||||
// #endif
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.network-container {
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.status-card,
|
||||
.test-card {
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.status-header,
|
||||
.test-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 4px 12px;
|
||||
font-size: 14px;
|
||||
border-radius: 12px;
|
||||
|
||||
&.online {
|
||||
color: #4caf50;
|
||||
background-color: #e8f5e9;
|
||||
}
|
||||
|
||||
&.offline {
|
||||
color: #f44336;
|
||||
background-color: #ffebee;
|
||||
}
|
||||
}
|
||||
|
||||
.status-info,
|
||||
.test-content {
|
||||
.info-item,
|
||||
.test-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.label {
|
||||
width: 100px;
|
||||
font-size: 15px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.value {
|
||||
flex: 1;
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 2px 8px;
|
||||
font-size: 13px;
|
||||
border-radius: 4px;
|
||||
|
||||
&.good {
|
||||
color: #4caf50;
|
||||
background-color: #e8f5e9;
|
||||
}
|
||||
|
||||
&.normal {
|
||||
color: #ff9800;
|
||||
background-color: #fff3e0;
|
||||
}
|
||||
|
||||
&.bad {
|
||||
color: #f44336;
|
||||
background-color: #ffebee;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.test-header {
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 4px;
|
||||
margin: 20px 0;
|
||||
overflow: hidden;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 2px;
|
||||
|
||||
.progress {
|
||||
height: 100%;
|
||||
background-color: #409eff;
|
||||
transition: width 0.2s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
.test-btn {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
margin-top: 20px;
|
||||
font-size: 16px;
|
||||
line-height: 44px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
background-color: #409eff;
|
||||
border: none;
|
||||
border-radius: 22px;
|
||||
|
||||
&:active {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
cursor: not-allowed;
|
||||
background-color: #a0cfff;
|
||||
}
|
||||
}
|
||||
</style>
|
103
src/pages/mine/settings/privacy/index.vue
Normal file
103
src/pages/mine/settings/privacy/index.vue
Normal file
@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<view class="privacy-container">
|
||||
<view class="privacy-header">
|
||||
<text class="header-title">隐私政策</text>
|
||||
<text class="header-date">更新日期:2024年3月15日</text>
|
||||
</view>
|
||||
|
||||
<view class="privacy-content">
|
||||
<view v-for="(section, index) in privacyContent" :key="index" class="section">
|
||||
<text class="section-title">{{ section.title }}</text>
|
||||
<text class="section-text">{{ section.content }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const privacyContent = ref([
|
||||
{
|
||||
title: "1. 信息收集",
|
||||
content:
|
||||
"我们可能收集您的基本信息(如设备信息、操作日志等)以提供更好的服务体验。我们承诺对这些信息进行严格保密,并只用于改善产品服务。",
|
||||
},
|
||||
{
|
||||
title: "2. 信息使用",
|
||||
content:
|
||||
"收集的信息将用于:优化用户体验、提供客户支持、发送重要通知、保障账号安全等。未经您的同意,我们不会向第三方分享您的个人信息。",
|
||||
},
|
||||
{
|
||||
title: "3. 信息安全",
|
||||
content:
|
||||
"我们采用业界标准的安全技术和程序来保护您的个人信息,防止未经授权的访问、使用或泄露。我们定期审查信息收集、存储和处理实践。",
|
||||
},
|
||||
{
|
||||
title: "4. Cookie 使用",
|
||||
content:
|
||||
"我们使用 Cookie 和类似技术来提供、保护和改进我们的产品和服务。这些技术帮助我们了解用户行为,告诉我们哪些功能最受欢迎。",
|
||||
},
|
||||
{
|
||||
title: "5. 第三方服务",
|
||||
content:
|
||||
"我们的应用可能包含第三方服务。这些第三方服务有自己的隐私政策,我们建议您查看这些政策。我们不对第三方的隐私实践负责。",
|
||||
},
|
||||
{
|
||||
title: "6. 未成年人保护",
|
||||
content:
|
||||
"我们非常重视对未成年人个人信息的保护。若您是未成年人,请在监护人指导下使用我们的服务。如果您是监护人,当您对您所监护的未成年人的个人信息有疑问时,请联系我们。",
|
||||
},
|
||||
{
|
||||
title: "7. 隐私政策更新",
|
||||
content:
|
||||
"我们可能会不时更新本隐私政策。当我们更新隐私政策时,我们会在本页面上发布更新后的版本并修改更新日期。建议您定期查看本页面。",
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.privacy-container {
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.privacy-header {
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
|
||||
.header-title {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.header-date {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.privacy-content {
|
||||
.section {
|
||||
margin-bottom: 25px;
|
||||
|
||||
.section-title {
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
font-size: 17px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.section-text {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
color: #666;
|
||||
text-align: justify;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
304
src/pages/mine/settings/theme/index.vue
Normal file
304
src/pages/mine/settings/theme/index.vue
Normal file
@ -0,0 +1,304 @@
|
||||
<template>
|
||||
<view class="theme-settings-container">
|
||||
<view class="title">主题色设置</view>
|
||||
|
||||
<view class="color-palette">
|
||||
<view class="subtitle">选择主题色</view>
|
||||
<view class="color-grid">
|
||||
<view
|
||||
v-for="(color, index) in themeColors"
|
||||
:key="index"
|
||||
class="color-item"
|
||||
:style="{ backgroundColor: color }"
|
||||
@click="handleSelectColor(color)"
|
||||
>
|
||||
<wd-icon v-if="themeStore.primaryColor === color" name="check" color="#fff" size="16" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="custom-color">
|
||||
<view class="subtitle">自定义主题色</view>
|
||||
<view class="color-picker">
|
||||
<view class="color-preview" :style="{ backgroundColor: customColor }"></view>
|
||||
<input
|
||||
v-model="customColor"
|
||||
type="text"
|
||||
placeholder="请输入十六进制颜色值,如 #165DFF"
|
||||
class="color-input"
|
||||
maxlength="7"
|
||||
/>
|
||||
</view>
|
||||
<button class="apply-btn" @click="applyCustomColor">应用</button>
|
||||
</view>
|
||||
|
||||
<view class="preview-section">
|
||||
<view class="subtitle">预览效果</view>
|
||||
<view class="preview-container">
|
||||
<view class="preview-item">
|
||||
<view class="preview-button" :style="{ backgroundColor: themeStore.primaryColor }">
|
||||
按钮
|
||||
</view>
|
||||
</view>
|
||||
<view class="preview-item">
|
||||
<view class="preview-text" :style="{ color: themeStore.primaryColor }">文本颜色</view>
|
||||
</view>
|
||||
<view class="preview-item">
|
||||
<view class="preview-border" :style="{ borderColor: themeStore.primaryColor }">边框</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="reset-btn" @click="resetTheme">恢复默认主题色</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||||
import { useThemeStore } from "@/store";
|
||||
import { applyThemeOnPageShow } from "@/utils/theme";
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
// 预设主题色
|
||||
const themeColors = [
|
||||
"#165DFF", // Arco蓝色
|
||||
"#0FC6C2", // 青绿色
|
||||
"#722ED1", // 紫色
|
||||
"#F5222D", // 红色
|
||||
"#FA8C16", // 橙色
|
||||
"#FADB14", // 黄色
|
||||
"#52C41A", // 绿色
|
||||
"#EB2F96", // 粉色
|
||||
];
|
||||
|
||||
// 自定义颜色
|
||||
const customColor = ref("#165DFF");
|
||||
|
||||
// 选择预设颜色
|
||||
const handleSelectColor = (color: string) => {
|
||||
themeStore.setPrimaryColor(color);
|
||||
customColor.value = color;
|
||||
|
||||
// 保存设置
|
||||
saveThemeSettings();
|
||||
|
||||
// 提示
|
||||
uni.showToast({
|
||||
title: "主题色已更新",
|
||||
icon: "success",
|
||||
});
|
||||
};
|
||||
|
||||
// 应用自定义颜色
|
||||
const applyCustomColor = () => {
|
||||
// 验证颜色格式
|
||||
const colorRegex = /^#([0-9A-F]{6})$/i;
|
||||
if (!colorRegex.test(customColor.value)) {
|
||||
uni.showToast({
|
||||
title: "请输入有效的颜色值",
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
themeStore.setPrimaryColor(customColor.value);
|
||||
|
||||
// 保存设置
|
||||
saveThemeSettings();
|
||||
|
||||
// 提示
|
||||
uni.showToast({
|
||||
title: "自定义主题色已应用",
|
||||
icon: "success",
|
||||
});
|
||||
};
|
||||
|
||||
// 重置为默认主题色
|
||||
const resetTheme = () => {
|
||||
const defaultColor = "#165DFF"; // Arco蓝色
|
||||
themeStore.setPrimaryColor(defaultColor);
|
||||
customColor.value = defaultColor;
|
||||
|
||||
// 保存设置
|
||||
saveThemeSettings();
|
||||
|
||||
// 提示
|
||||
uni.showToast({
|
||||
title: "已恢复默认主题色",
|
||||
icon: "success",
|
||||
});
|
||||
};
|
||||
|
||||
// 保存主题设置
|
||||
const saveThemeSettings = () => {
|
||||
// 主题store已经处理了持久化,这里不需要额外操作
|
||||
};
|
||||
|
||||
onLoad(() => {
|
||||
// 初始化自定义颜色输入框
|
||||
customColor.value = themeStore.primaryColor;
|
||||
});
|
||||
|
||||
// 页面显示时重新应用主题(解决小程序环境问题)
|
||||
onShow(() => {
|
||||
// 在小程序环境中,页面显示时重新应用主题
|
||||
if (typeof document === "undefined") {
|
||||
applyThemeOnPageShow(themeStore.primaryColor);
|
||||
|
||||
// 为了确保样式应用,我们可以在页面中设置内联样式
|
||||
// 这样可以绕过小程序的CSS变量限制
|
||||
const pages = getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
if (currentPage) {
|
||||
console.log("当前页面重新应用主题色");
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.theme-settings-container {
|
||||
padding: 30rpx;
|
||||
|
||||
.title {
|
||||
margin-bottom: 30rpx;
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin-bottom: 20rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.color-palette {
|
||||
padding: 30rpx;
|
||||
margin-bottom: 40rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.color-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
|
||||
.color-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-color {
|
||||
padding: 30rpx;
|
||||
margin-bottom: 40rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.color-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.color-preview {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
margin-right: 20rpx;
|
||||
border-radius: 8rpx;
|
||||
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.color-input {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 28rpx;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.apply-btn {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 80rpx;
|
||||
color: #fff;
|
||||
background-color: var(--primary-color);
|
||||
border: none;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-section {
|
||||
padding: 30rpx;
|
||||
margin-bottom: 40rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.preview-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
|
||||
.preview-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.preview-button {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.preview-text {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.preview-border {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
text-align: center;
|
||||
background-color: #f5f5f5;
|
||||
border: 2px solid;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 80rpx;
|
||||
color: #333;
|
||||
background-color: #f5f5f5;
|
||||
border: none;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
273
src/pages/todo/index.vue
Normal file
273
src/pages/todo/index.vue
Normal file
@ -0,0 +1,273 @@
|
||||
<template>
|
||||
<view class="todo-container">
|
||||
<view class="page-header">
|
||||
<text class="page-title">待办事项</text>
|
||||
<wd-button size="small" type="primary" icon="add" @click="showAddTodoPopup = true">
|
||||
新建待办
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<view class="filter-tabs">
|
||||
<wd-tabs v-model="activeTab" sticky>
|
||||
<wd-tab title="全部" name="all" />
|
||||
<wd-tab title="未完成" name="active" />
|
||||
<wd-tab title="已完成" name="completed" />
|
||||
</wd-tabs>
|
||||
</view>
|
||||
|
||||
<TodoList :todos="filteredTodos" @update="handleUpdateTodo" @delete="handleDeleteTodo" />
|
||||
|
||||
<!-- 新增待办弹窗 -->
|
||||
<wd-popup
|
||||
v-model="showAddTodoPopup"
|
||||
position="bottom"
|
||||
close-on-click-modal
|
||||
:style="{ height: '65%' }"
|
||||
>
|
||||
<view class="popup-header">
|
||||
<text class="popup-title">新建待办</text>
|
||||
<wd-icon name="close" @click="showAddTodoPopup = false" />
|
||||
</view>
|
||||
<view class="popup-form">
|
||||
<wd-input
|
||||
v-model="newTodo.title"
|
||||
placeholder="请输入待办标题"
|
||||
:rules="[{ required: true, message: '请输入标题' }]"
|
||||
/>
|
||||
<wd-textarea
|
||||
v-model="newTodo.description"
|
||||
placeholder="请输入详细描述"
|
||||
rows="3"
|
||||
autosize
|
||||
class="mt-20"
|
||||
/>
|
||||
<wd-cell title="截止日期" is-link @click="showDatePicker = true">
|
||||
<text v-if="newTodo.deadline">{{ formatDate(newTodo.deadline) }}</text>
|
||||
<text v-else class="text-placeholder">请选择</text>
|
||||
</wd-cell>
|
||||
<wd-cell title="优先级">
|
||||
<wd-radio-group v-model="newTodo.priority" shape="button">
|
||||
<wd-radio value="low">低</wd-radio>
|
||||
<wd-radio value="medium">中</wd-radio>
|
||||
<wd-radio value="high">高</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
<view class="form-actions">
|
||||
<wd-button
|
||||
block
|
||||
type="primary"
|
||||
:loading="loading"
|
||||
:disabled="!newTodo.title"
|
||||
@click="handleAddTodo"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
||||
<!-- 日期选择器 -->
|
||||
<wd-datetime-picker
|
||||
v-model="showDatePicker"
|
||||
v-model:value="newTodo.deadline"
|
||||
label="截止日期"
|
||||
type="date"
|
||||
confirm-button-text="确认"
|
||||
cancel-button-text="取消"
|
||||
title="选择截止日期"
|
||||
/>
|
||||
|
||||
<wd-toast />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from "vue";
|
||||
import { useToast } from "wot-design-uni";
|
||||
import { Todo } from "@/types/todo";
|
||||
import TodoList from "@/components/todo/TodoList.vue";
|
||||
|
||||
const toast = useToast();
|
||||
const loading = ref(false);
|
||||
const showAddTodoPopup = ref(false);
|
||||
const showDatePicker = ref(false);
|
||||
const activeTab = ref("all");
|
||||
|
||||
// 待办列表
|
||||
const todos = ref<Todo[]>([
|
||||
{
|
||||
id: "1",
|
||||
title: "完成个人资料填写",
|
||||
description: "包括上传头像、填写基本信息等",
|
||||
completed: false,
|
||||
priority: "high",
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "阅读使用指南",
|
||||
description: "熟悉系统功能和操作流程",
|
||||
completed: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
]);
|
||||
|
||||
// 新增待办表单
|
||||
const newTodo = ref<Partial<Todo>>({
|
||||
title: "",
|
||||
description: "",
|
||||
priority: "medium",
|
||||
deadline: "",
|
||||
});
|
||||
|
||||
// 根据标签筛选待办
|
||||
const filteredTodos = computed(() => {
|
||||
switch (activeTab.value) {
|
||||
case "active":
|
||||
return todos.value.filter((todo) => !todo.completed);
|
||||
case "completed":
|
||||
return todos.value.filter((todo) => todo.completed);
|
||||
default:
|
||||
return todos.value;
|
||||
}
|
||||
});
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (date: string) => {
|
||||
const d = new Date(date);
|
||||
const year = d.getFullYear();
|
||||
const month = String(d.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(d.getDate()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
// 生成唯一ID
|
||||
const generateId = () => {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substring(2);
|
||||
};
|
||||
|
||||
// 添加待办
|
||||
const handleAddTodo = () => {
|
||||
if (!newTodo.value.title) {
|
||||
toast.error("请输入待办标题");
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
// 模拟API调用
|
||||
setTimeout(() => {
|
||||
const now = new Date().toISOString();
|
||||
const todo: Todo = {
|
||||
id: generateId(),
|
||||
title: newTodo.value.title!,
|
||||
description: newTodo.value.description,
|
||||
completed: false,
|
||||
deadline: newTodo.value.deadline,
|
||||
priority: newTodo.value.priority as "low" | "medium" | "high",
|
||||
createdAt: now,
|
||||
};
|
||||
|
||||
todos.value.unshift(todo);
|
||||
resetForm();
|
||||
loading.value = false;
|
||||
showAddTodoPopup.value = false;
|
||||
toast.success("待办创建成功");
|
||||
}, 500);
|
||||
};
|
||||
|
||||
// 更新待办
|
||||
const handleUpdateTodo = (todo: Todo) => {
|
||||
const index = todos.value.findIndex((item) => item.id === todo.id);
|
||||
if (index !== -1) {
|
||||
todos.value[index] = {
|
||||
...todo,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
toast.success(todo.completed ? "已完成" : "已取消完成");
|
||||
}
|
||||
};
|
||||
|
||||
// 删除待办
|
||||
const handleDeleteTodo = (id: string) => {
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: "确认删除该待办事项?",
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
todos.value = todos.value.filter((todo) => todo.id !== id);
|
||||
toast.success("删除成功");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
newTodo.value = {
|
||||
title: "",
|
||||
description: "",
|
||||
priority: "medium",
|
||||
deadline: "",
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.todo-container {
|
||||
min-height: 100vh;
|
||||
padding: 30rpx;
|
||||
background-color: #f5f7fa;
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.page-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-tabs {
|
||||
margin-bottom: 20rpx;
|
||||
overflow: hidden;
|
||||
background-color: #fff;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
|
||||
.popup-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.popup-form {
|
||||
padding: 30rpx;
|
||||
|
||||
.form-actions {
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.text-placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.mt-20 {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
324
src/pages/work/config/index.vue
Normal file
324
src/pages/work/config/index.vue
Normal file
@ -0,0 +1,324 @@
|
||||
<template>
|
||||
<view class="work">
|
||||
<!-- 筛选 -->
|
||||
<wd-drop-menu close-on-click-modal class="mb-20rpx mr-20rpx ml-20rpx">
|
||||
<wd-drop-menu-item ref="dropMenu" title="筛选" icon="filter" icon-size="18px">
|
||||
<view>
|
||||
<wd-input
|
||||
v-model="queryParams.keywords"
|
||||
label="关键字"
|
||||
type="text"
|
||||
clearable
|
||||
placeholder="请输入关键字"
|
||||
/>
|
||||
<view class="flex flex-row items-center mb-20rpx">
|
||||
<wd-button class="mt-20rpx mb-20rpx" size="medium" @click="handleQuery()">
|
||||
查询
|
||||
</wd-button>
|
||||
<wd-button size="medium" type="info" @click="handleReset">重置</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-drop-menu-item>
|
||||
</wd-drop-menu>
|
||||
|
||||
<view class="data-container">
|
||||
<!-- 列表内容 -->
|
||||
<view v-for="(item, index) in pageData" :key="index" class="mb-20rpx">
|
||||
<wd-card>
|
||||
<template #title>
|
||||
<view class="flex items-center justify-between">
|
||||
<view class="flex-1 text-truncate">{{ item.configName }}</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<wd-cell-group>
|
||||
<wd-cell title="配置键" :value="item.configKey" />
|
||||
<wd-cell title="配置值" :value="item.configValue" />
|
||||
<wd-cell title="备注" :value="item.remark" />
|
||||
</wd-cell-group>
|
||||
|
||||
<template #footer>
|
||||
<wd-button size="small" plain type="primary" @click="handleAction(item)">
|
||||
操作
|
||||
</wd-button>
|
||||
</template>
|
||||
</wd-card>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<wd-loadmore :state="state" @reload="handleQuery" />
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<view class="fixed bottom-0 w-full flex justify-around items-center p-20rpx bg-#fff">
|
||||
<wd-button size="medium" type="primary" @click="handleOpenDialog">添加</wd-button>
|
||||
<wd-button size="medium" type="success" @click="refreshCache">刷新缓存</wd-button>
|
||||
</view>
|
||||
|
||||
<wd-popup v-model="showEditPopup" position="bottom">
|
||||
<view class="p-20rpx">
|
||||
<wd-form ref="formRef" :model="form">
|
||||
<wd-input
|
||||
v-model="form.configName"
|
||||
label="配置名称"
|
||||
type="text"
|
||||
placeholder="请输入配置名称"
|
||||
:rules="[{ required: true, message: '请填写配置名称' }]"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="form.configKey"
|
||||
label="配置键名"
|
||||
type="text"
|
||||
placeholder="请输入配置键名"
|
||||
:rules="[{ required: true, message: '请填写配置键' }]"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="form.configValue"
|
||||
label="配置键值"
|
||||
type="text"
|
||||
placeholder="请输入配置键值"
|
||||
:rules="[{ required: true, message: '请填写配置值' }]"
|
||||
/>
|
||||
<wd-textarea
|
||||
v-model="form.remark"
|
||||
prop="remark"
|
||||
label="配置描述"
|
||||
label-align="right"
|
||||
clearable
|
||||
:maxlength="100"
|
||||
show-word-limit
|
||||
label-width="100px"
|
||||
placeholder="请输入配置描述"
|
||||
/>
|
||||
</wd-form>
|
||||
<view class="flex justify-around mt-20rpx">
|
||||
<wd-button @click="showEditPopup = false">取消</wd-button>
|
||||
<wd-button type="primary" native-type="submit" :loading="loading" @click="submitForm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ConfigAPI, { ConfigPageVO, ConfigForm, ConfigPageQuery } from "@/api/system/config";
|
||||
import { DropMenuItemExpose } from "wot-design-uni/components/wd-drop-menu-item/types";
|
||||
import { LoadMoreState } from "wot-design-uni/components/wd-loadmore/types";
|
||||
import { FormInstance } from "wot-design-uni/components/wd-form/types";
|
||||
import { debounce } from "@/utils";
|
||||
const state = ref<LoadMoreState>("loading"); // 加载状态 loading, finished:, error
|
||||
const total = ref(0);
|
||||
const queryParams = reactive<ConfigPageQuery>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
keywords: "",
|
||||
});
|
||||
// 系统配置表格数据
|
||||
const pageData = ref<ConfigPageVO[]>([]);
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
/**
|
||||
* 搜索栏
|
||||
*/
|
||||
const dropMenu = ref<DropMenuItemExpose>();
|
||||
|
||||
/**
|
||||
* 搜索
|
||||
*/
|
||||
function handleQuery() {
|
||||
pageData.value = [];
|
||||
dropMenu.value?.close();
|
||||
queryParams.pageNum = 1;
|
||||
loadmore();
|
||||
}
|
||||
|
||||
const showEditPopup = ref(false);
|
||||
const form = ref<ConfigForm>({});
|
||||
|
||||
// 修改编辑函数
|
||||
function handleEdit(id: number) {
|
||||
ConfigAPI.getFormData(id).then((data) => {
|
||||
Object.assign(form.value, data);
|
||||
// 显示弹窗
|
||||
showEditPopup.value = true;
|
||||
});
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
async function submitForm() {
|
||||
loading.value = true;
|
||||
try {
|
||||
if (formRef.value) {
|
||||
formRef.value!.validate().then(async ({ valid }) => {
|
||||
if (valid) {
|
||||
if (form.value.id) {
|
||||
await ConfigAPI.update(form.value.id, form.value);
|
||||
uni.showToast({ title: "更新成功", icon: "success" });
|
||||
} else {
|
||||
await ConfigAPI.add(form.value);
|
||||
uni.showToast({ title: "添加成功", icon: "success" });
|
||||
}
|
||||
showEditPopup.value = false;
|
||||
handleQuery(); // 刷新列表
|
||||
} else {
|
||||
uni.showToast({ title: "请检查表单", icon: "error" });
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
uni.showToast({ title: "操作失败", icon: "error" });
|
||||
loading.value = false;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置搜索条件
|
||||
*/
|
||||
function handleReset() {
|
||||
queryParams.pageNum = 1;
|
||||
queryParams.keywords = "";
|
||||
pageData.value = [];
|
||||
dropMenu.value?.close();
|
||||
handleQuery();
|
||||
}
|
||||
|
||||
/**
|
||||
* 触底事件
|
||||
*/
|
||||
onReachBottom(() => {
|
||||
queryParams.pageNum++;
|
||||
handleQuery();
|
||||
});
|
||||
|
||||
/**
|
||||
* 加载更多
|
||||
*/
|
||||
function loadmore() {
|
||||
state.value = "loading";
|
||||
ConfigAPI.getPage(queryParams)
|
||||
.then((data) => {
|
||||
if (queryParams.pageNum === 1) {
|
||||
pageData.value = data.list;
|
||||
} else {
|
||||
pageData.value.push(...data.list);
|
||||
}
|
||||
total.value = data.total;
|
||||
})
|
||||
.finally(() => {
|
||||
state.value = "finished";
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加
|
||||
*/
|
||||
function handleOpenDialog() {
|
||||
form.value.id = undefined;
|
||||
form.value.configName = "";
|
||||
form.value.configKey = "";
|
||||
form.value.configValue = "";
|
||||
form.value.remark = "";
|
||||
showEditPopup.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新缓存
|
||||
*/
|
||||
const refreshCache = debounce(() => {
|
||||
ConfigAPI.refreshCache().then(() => {
|
||||
uni.showToast({
|
||||
title: "刷新缓存成功",
|
||||
icon: "success",
|
||||
duration: 1000, // 显示时间,单位为毫秒,设置为 0 则不会自动消失
|
||||
});
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*/
|
||||
function handleDelete(item: ConfigPageVO) {
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: "确定要删除该配置吗?",
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
if (item.id) {
|
||||
ConfigAPI.deleteById(item.id).then(() => {
|
||||
uni.showToast({ title: "删除成功", icon: "success" });
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作
|
||||
*/
|
||||
function handleAction(item: ConfigPageVO) {
|
||||
const actions = ["编辑", "删除"];
|
||||
uni.showActionSheet({
|
||||
itemList: actions,
|
||||
success: ({ tapIndex }) => {
|
||||
switch (actions[tapIndex]) {
|
||||
case "编辑":
|
||||
handleEdit(item.id || 0);
|
||||
break;
|
||||
case "删除":
|
||||
handleDelete(item);
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
handleQuery();
|
||||
});
|
||||
|
||||
/**
|
||||
* 页面返回回来也要重新加载数据
|
||||
*/
|
||||
onLoad(() => {
|
||||
queryParams.pageNum = 1;
|
||||
handleQuery();
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.data-container {
|
||||
:deep(.wd-cell__wrapper) {
|
||||
padding: 4rpx 0;
|
||||
}
|
||||
|
||||
:deep(.wd-cell) {
|
||||
padding-right: 10rpx;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
|
||||
:deep(.wd-fab__trigger) {
|
||||
width: 80rpx !important;
|
||||
height: 80rpx !important;
|
||||
}
|
||||
|
||||
:deep(.wd-cell__right) {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
padding: 10rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.data-container {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
81
src/pages/work/index.vue
Normal file
81
src/pages/work/index.vue
Normal file
@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<view class="work">
|
||||
<template v-for="(item, index) in gridList" :key="index">
|
||||
<wd-card :title="item.title">
|
||||
<wd-grid clickable :column="4">
|
||||
<wd-grid-item
|
||||
v-for="(child, index) in item.children"
|
||||
:key="index"
|
||||
:v-has-perm="child.prem"
|
||||
use-slot
|
||||
link-type="navigateTo"
|
||||
:url="child.url"
|
||||
>
|
||||
<view class="p-2">
|
||||
<image class="w-72rpx h-72rpx rounded-8rpx" :src="child.icon" />
|
||||
</view>
|
||||
<view class="text">{{ child.title }}</view>
|
||||
</wd-grid-item>
|
||||
</wd-grid>
|
||||
</wd-card>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const gridList = reactive([
|
||||
{
|
||||
title: "系统管理",
|
||||
children: [
|
||||
{
|
||||
icon: "/static/icons/user.png",
|
||||
title: "用户管理",
|
||||
url: "/pages/work/user/index",
|
||||
prem: "sys:user:query",
|
||||
},
|
||||
{
|
||||
icon: "/static/icons/role.png",
|
||||
title: "角色管理",
|
||||
url: "/pages/work/role/index",
|
||||
prem: "sys:role:query",
|
||||
},
|
||||
|
||||
{
|
||||
icon: "/static/icons/notice.png",
|
||||
title: "通知公告",
|
||||
url: "/pages/work/notice/index",
|
||||
prem: "sys:notice:query",
|
||||
},
|
||||
{
|
||||
icon: "/static/icons/setting.png",
|
||||
title: "系统配置",
|
||||
url: "/pages/work/config/index",
|
||||
prem: "sys:config:query",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "系统监控",
|
||||
children: [
|
||||
{
|
||||
icon: "/static/icons/log.png",
|
||||
title: "系统日志",
|
||||
url: "/pages/work/log/index",
|
||||
prem: "sys:log:query",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* stylelint-disable selector-type-no-unknown */
|
||||
page {
|
||||
background: #f8f8f8;
|
||||
}
|
||||
/* stylelint-enable selector-type-no-unknown */
|
||||
|
||||
.work {
|
||||
padding: 40rpx 0;
|
||||
}
|
||||
</style>
|
174
src/pages/work/log/index.vue
Normal file
174
src/pages/work/log/index.vue
Normal file
@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<view class="log">
|
||||
<!-- 筛选 -->
|
||||
<wd-drop-menu close-on-click-modal class="mb-24rpx">
|
||||
<wd-drop-menu-item ref="filterDropMenu" title="筛选" icon="filter" icon-size="18px">
|
||||
<view>
|
||||
<wd-input
|
||||
v-model="queryParams.keywords"
|
||||
label="关键字"
|
||||
type="text"
|
||||
placeholder="请输入关键字"
|
||||
/>
|
||||
|
||||
<cu-date-query v-model="queryParams.createTime" :label="'日期选择'" />
|
||||
|
||||
<view class="flex-between py-2">
|
||||
<wd-button class="w-20%" type="info" @click="handleResetQuery">重置</wd-button>
|
||||
<wd-button class="w-70%" @click="handleQuery">查询</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-drop-menu-item>
|
||||
</wd-drop-menu>
|
||||
|
||||
<!-- 卡片列表 -->
|
||||
<wd-card v-for="item in pageData" :key="item.id" class="card-list">
|
||||
<template #title>
|
||||
{{ item.operator }}
|
||||
</template>
|
||||
|
||||
<wd-cell-group>
|
||||
<wd-cell title="模块" :value="item.module" />
|
||||
<wd-cell title="内容" :value="item.content" />
|
||||
<wd-cell title="IP" :value="item.ip" />
|
||||
<wd-cell title="地区" :value="item.region" />
|
||||
</wd-cell-group>
|
||||
|
||||
<template #footer>
|
||||
<view class="flex-between">
|
||||
<view class="text-left">
|
||||
<wd-text text="创建时间:" size="small" class="font-bold" />
|
||||
<wd-text :text="item.createTime" size="small" />
|
||||
</view>
|
||||
<view class="text-right">
|
||||
<wd-button type="primary" size="small" plain @click="handleViewDetail(item)">
|
||||
查看详情
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</wd-card>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<wd-popup v-model="detailDialogVisible" position="bottom">
|
||||
<wd-cell-group>
|
||||
<wd-cell title="操作人" :value="logDetail.operator" />
|
||||
<wd-cell title="操作时间" :value="logDetail.createTime" />
|
||||
<wd-cell title="模块" :value="logDetail.module" />
|
||||
<wd-cell title="内容" :value="logDetail.content" />
|
||||
<wd-cell title="IP" :value="logDetail.ip" />
|
||||
<wd-cell title="地区" :value="logDetail.region" />
|
||||
<wd-cell title="浏览器" :value="logDetail.region" />
|
||||
<wd-cell title="终端系统" :value="logDetail.os" />
|
||||
<wd-cell title="耗时(毫秒)" :value="logDetail.executionTime" />
|
||||
</wd-cell-group>
|
||||
</wd-popup>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<wd-loadmore v-if="total > 0" :state="loadMoreState" @reload="loadmore" />
|
||||
<wd-status-tip v-else-if="total == 0" image="search" tip="当前搜索无结果" />
|
||||
</view>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onLoad, onReachBottom } from "@dcloudio/uni-app";
|
||||
import { LoadMoreState } from "wot-design-uni/components/wd-loadmore/types";
|
||||
import { DropMenuItemExpose } from "wot-design-uni/components/wd-drop-menu-item/types";
|
||||
|
||||
import LogAPI, { LogVO, LogPageQuery } from "@/api/system/log";
|
||||
|
||||
const filterDropMenu = ref<DropMenuItemExpose>();
|
||||
const loadMoreState = ref<LoadMoreState>("loading");
|
||||
|
||||
const queryParams = reactive<LogPageQuery>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const total = ref(0);
|
||||
const pageData = ref<LogVO[]>([]);
|
||||
|
||||
const logDetail = ref<LogVO>({});
|
||||
const detailDialogVisible = ref(false);
|
||||
|
||||
/**
|
||||
* 搜索栏
|
||||
*/
|
||||
function handleQuery() {
|
||||
filterDropMenu.value?.close();
|
||||
queryParams.pageNum = 1;
|
||||
loadmore();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置搜索
|
||||
*/
|
||||
function handleResetQuery() {
|
||||
queryParams.keywords = undefined;
|
||||
queryParams.createTime = undefined;
|
||||
handleQuery();
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载更多
|
||||
*/
|
||||
function loadmore() {
|
||||
loadMoreState.value = "loading";
|
||||
LogAPI.getPage(queryParams)
|
||||
.then((data) => {
|
||||
pageData.value = data.list;
|
||||
total.value = data.total;
|
||||
queryParams.pageNum++;
|
||||
})
|
||||
.catch(() => {
|
||||
pageData.value = [];
|
||||
})
|
||||
.finally(() => {
|
||||
loadMoreState.value = "finished";
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看详情
|
||||
*/
|
||||
function handleViewDetail(item: LogVO) {
|
||||
detailDialogVisible.value = true;
|
||||
logDetail.value = item;
|
||||
}
|
||||
|
||||
/**
|
||||
* 触底事件
|
||||
*/
|
||||
onReachBottom(() => {
|
||||
if (queryParams.pageNum * queryParams.pageSize < total.value) {
|
||||
loadmore();
|
||||
} else if (queryParams.pageNum * queryParams.pageSize >= total.value) {
|
||||
loadMoreState.value = "finished";
|
||||
}
|
||||
});
|
||||
|
||||
onLoad(() => {
|
||||
handleQuery();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wd-col {
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
::v-deep .wd-drop-menu__item {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 0 50rpx;
|
||||
}
|
||||
|
||||
.card-list {
|
||||
:deep(.wd-cell__wrapper) {
|
||||
padding: 4rpx 0;
|
||||
}
|
||||
:deep(.wd-cell) {
|
||||
padding-right: 10rpx;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
}
|
||||
</style>
|
117
src/pages/work/notice/detail.vue
Normal file
117
src/pages/work/notice/detail.vue
Normal file
@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div class="notice-detail">
|
||||
<wd-cell-group>
|
||||
<div class="notice-header">
|
||||
<h2 class="notice-title">
|
||||
<wd-icon name="prompt" size="20px" class="title-icon" />
|
||||
{{ noticeDetail.title }}
|
||||
</h2>
|
||||
<div class="notice-meta">
|
||||
<div class="meta-row">
|
||||
优先级:
|
||||
<cu-dict-label code="notice_level" :model-value="noticeDetail.level" />
|
||||
</div>
|
||||
<div class="meta-row">发布人:{{ noticeDetail.publisherName }}</div>
|
||||
<div class="meta-row">发布时间:{{ noticeDetail.publishTime }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<wd-divider />
|
||||
<div class="notice-content" v-html="noticeDetail.content" />
|
||||
</wd-cell-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import NoticeAPI, { NoticeDetailVO } from "@/api/system/notice";
|
||||
|
||||
const noticeDetail = ref<NoticeDetailVO>({});
|
||||
|
||||
/**
|
||||
* 获取通知详情
|
||||
* @param id 通知ID
|
||||
*/
|
||||
const getNoticeDetail = async (id: string) => {
|
||||
try {
|
||||
const res = await NoticeAPI.getDetail(id);
|
||||
noticeDetail.value = res;
|
||||
} catch (error) {
|
||||
console.error("获取通知详情失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 页面加载
|
||||
onLoad((options: any) => {
|
||||
if (options && options.id) {
|
||||
getNoticeDetail(options.id as string);
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: "通知不存在",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.notice-detail {
|
||||
padding: 12px;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
|
||||
.notice-header {
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.notice-title {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 12px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.notice-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.priority-wrapper {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.notice-content {
|
||||
padding: 16px;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
:deep(.wd-divider) {
|
||||
margin: 0 8px;
|
||||
}
|
||||
</style>
|
284
src/pages/work/notice/index.vue
Normal file
284
src/pages/work/notice/index.vue
Normal file
@ -0,0 +1,284 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- 添加搜索栏 -->
|
||||
<wd-drop-menu close-on-click-modal class="mb-20rpx mr-20rpx ml-20rpx">
|
||||
<wd-drop-menu-item ref="dropMenu" title="筛选" icon="filter" icon-size="18px">
|
||||
<view>
|
||||
<wd-input
|
||||
v-model="queryParams.title"
|
||||
label="关键字"
|
||||
type="text"
|
||||
placeholder="请输入关键字"
|
||||
/>
|
||||
|
||||
<view class="flex flex-row items-center mb-20rpx">
|
||||
<wd-button class="mt-20rpx mb-20rpx" size="medium" @click="handleQuery()">
|
||||
查询
|
||||
</wd-button>
|
||||
<wd-button size="medium" type="info" @click="handleReset">重置</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-drop-menu-item>
|
||||
</wd-drop-menu>
|
||||
|
||||
<!-- 列表内容 -->
|
||||
<view class="data-container">
|
||||
<view v-for="(item, index) in dataList" :key="index" class="mt-20rpx">
|
||||
<wd-card>
|
||||
<template #title>
|
||||
<view class="flex items-center justify-between">
|
||||
<view class="flex-1 text-truncate">{{ item.title }}</view>
|
||||
<wd-tag :type="getStatusType(item.publishStatus)" size="small">
|
||||
{{ getStatusText(item.publishStatus) }}
|
||||
</wd-tag>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<wd-cell-group>
|
||||
<wd-cell title="通告目标类型" :value="item.targetType === 1 ? '指定' : '全体'" />
|
||||
<wd-cell title="紧急程度">
|
||||
<template #default>
|
||||
<cu-dict-label code="notice_level" :model-value="item.level" />
|
||||
</template>
|
||||
</wd-cell>
|
||||
<wd-cell title="发布人" :value="item.publisherName || '-'" />
|
||||
</wd-cell-group>
|
||||
<template #footer>
|
||||
<view class="flex-between">
|
||||
<view v-if="item.publishStatus === 1" class="text-left">
|
||||
<wd-text text="发布时间:" size="small" class="font-bold" />
|
||||
<wd-text :text="formatDate(item.publishTime)" size="small" />
|
||||
</view>
|
||||
<view v-else class="text-left">
|
||||
<wd-text text="撤回时间:" size="small" class="font-bold" />
|
||||
<wd-text :text="formatDate(item.revokeTime)" size="small" />
|
||||
</view>
|
||||
<view class="text-right">
|
||||
<wd-button size="small" plain type="primary" @click="handleAction(item)">
|
||||
操作
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</wd-card>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<wd-loadmore custom-class="loadmore" :state="loadState" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { LoadMoreState } from "wot-design-uni/components/wd-loadmore/types";
|
||||
import { DropMenuItemExpose } from "wot-design-uni/components/wd-drop-menu-item/types";
|
||||
import NoticeAPI, { NoticePageQuery, NoticePageVO } from "@/api/system/notice";
|
||||
const loadState = ref<LoadMoreState>("finished");
|
||||
const dataList = ref<NoticePageVO[]>([]);
|
||||
const total = ref(0);
|
||||
|
||||
// 修改查询参数
|
||||
const queryParams = ref<NoticePageQuery>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
title: "",
|
||||
});
|
||||
|
||||
// 添加搜索处理函数
|
||||
const dropMenu = ref<DropMenuItemExpose>();
|
||||
const handleQuery = () => {
|
||||
queryParams.value.pageNum = 1;
|
||||
loadMore();
|
||||
dropMenu.value?.close();
|
||||
};
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
queryParams.value = { pageNum: 1, pageSize: 10 };
|
||||
dropMenu.value?.close();
|
||||
loadMore();
|
||||
};
|
||||
|
||||
// 获取状态样式
|
||||
const getStatusType = (
|
||||
status: number | undefined
|
||||
): "default" | "primary" | "danger" | "warning" | "success" => {
|
||||
if (!status) return "default";
|
||||
const statusMap: Record<number, "default" | "primary" | "danger" | "warning" | "success"> = {
|
||||
0: "primary",
|
||||
1: "success",
|
||||
[-1]: "warning",
|
||||
};
|
||||
return statusMap[status] || "default";
|
||||
};
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status: number | undefined): string => {
|
||||
if (status !== 0 && !status) return "-";
|
||||
const statusMap: Record<number, string> = {
|
||||
0: "未发布",
|
||||
1: "已发布",
|
||||
[-1]: "已撤回",
|
||||
};
|
||||
return statusMap[status] || "未知";
|
||||
};
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (date: Date | undefined): string => {
|
||||
return date ? date.toString() : "-";
|
||||
};
|
||||
|
||||
// 加载更多
|
||||
const loadMore = async () => {
|
||||
if (loadState.value === "loading") return;
|
||||
|
||||
loadState.value = "loading";
|
||||
const { list, total: totalCount } = await NoticeAPI.getPage(queryParams.value);
|
||||
|
||||
if (queryParams.value.pageNum === 1) {
|
||||
dataList.value = list;
|
||||
} else {
|
||||
dataList.value = [...dataList.value, ...list];
|
||||
}
|
||||
|
||||
total.value = totalCount;
|
||||
queryParams.value.pageNum++;
|
||||
|
||||
loadState.value = dataList.value.length >= total.value ? "finished" : "loading";
|
||||
};
|
||||
|
||||
// 查看详情
|
||||
const handleView = (notice: NoticePageVO) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/work/notice/detail?id=${notice.id}`,
|
||||
});
|
||||
};
|
||||
|
||||
// 操作按钮
|
||||
const handleAction = (notice: NoticePageVO) => {
|
||||
const actions = notice.publishStatus !== 1 ? ["查看", "删除", "发布"] : ["查看", "撤回"];
|
||||
uni.showActionSheet({
|
||||
itemList: actions,
|
||||
success: ({ tapIndex }) => {
|
||||
switch (actions[tapIndex]) {
|
||||
case "查看":
|
||||
handleView(notice);
|
||||
break;
|
||||
case "删除":
|
||||
handleDelete(notice);
|
||||
break;
|
||||
case "发布":
|
||||
handlePublish(notice);
|
||||
break;
|
||||
case "撤回":
|
||||
handleRevoke(notice);
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 删除
|
||||
const handleDelete = (notice: NoticePageVO) => {
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: "确定要删除该通知吗?",
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await NoticeAPI.deleteByIds(notice.id);
|
||||
uni.showToast({ title: "删除成功", icon: "success" });
|
||||
// 重新加载第一页
|
||||
queryParams.value.pageNum = 1;
|
||||
loadMore();
|
||||
} catch (error) {
|
||||
uni.showToast({ title: "删除失败" + error, icon: "none" });
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 发布
|
||||
const handlePublish = (notice: NoticePageVO) => {
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: "确定要发布该通知吗?",
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await NoticeAPI.publish(Number(notice.id));
|
||||
uni.showToast({ title: "发布成功", icon: "success" });
|
||||
// 重新加载第一页
|
||||
queryParams.value.pageNum = 1;
|
||||
loadMore();
|
||||
} catch (error) {
|
||||
uni.showToast({ title: "发布失败" + error, icon: "none" });
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 撤回
|
||||
const handleRevoke = (notice: NoticePageVO) => {
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: "确定要撤回该通知吗?",
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await NoticeAPI.revoke(Number(notice.id));
|
||||
uni.showToast({
|
||||
title: "撤回成功",
|
||||
icon: "success",
|
||||
});
|
||||
// 刷新列表
|
||||
queryParams.value.pageNum = 1;
|
||||
loadMore();
|
||||
} catch (error) {
|
||||
uni.showToast({
|
||||
title: "撤回失败" + error,
|
||||
icon: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
onReachBottom(() => {
|
||||
if (loadState.value === "loading" || loadState.value === "finished") return;
|
||||
loadMore();
|
||||
});
|
||||
|
||||
onLoad(() => {
|
||||
loadMore();
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.data-container {
|
||||
:deep(.wd-cell__wrapper) {
|
||||
padding: 4rpx 0;
|
||||
}
|
||||
|
||||
:deep(.wd-cell) {
|
||||
padding-right: 10rpx;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
|
||||
:deep(.wd-fab__trigger) {
|
||||
width: 80rpx !important;
|
||||
height: 80rpx !important;
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
padding: 10rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.data-container {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
109
src/pages/work/role/assign-perm.vue
Normal file
109
src/pages/work/role/assign-perm.vue
Normal file
@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<view>
|
||||
<view class="p-l-8 p-b-20">
|
||||
<DaTree
|
||||
ref="DaTreeRef"
|
||||
:data="menuPermOptions"
|
||||
:defaultExpandAll="true"
|
||||
:showCheckbox="true"
|
||||
/>
|
||||
</view>
|
||||
<wd-fab v-model:active="showFab" type="primary" position="right-top" direction="bottom">
|
||||
<wd-button custom-class="custom-button" type="primary" @click="doCheckedTree(rootKeys, true)">
|
||||
全 选
|
||||
</wd-button>
|
||||
<wd-button custom-class="custom-button" type="error" @click="doCheckedTree(rootKeys, false)">
|
||||
全不选
|
||||
</wd-button>
|
||||
<wd-button custom-class="custom-button" type="success" @click="doExpandTree('all', true)">
|
||||
全展开
|
||||
</wd-button>
|
||||
<wd-button custom-class="custom-button" type="warning" @click="doExpandTree('all', false)">
|
||||
全收起
|
||||
</wd-button>
|
||||
</wd-fab>
|
||||
<!-- 底部按钮 -->
|
||||
<view class="footer-buttons" style="">
|
||||
<wd-button size="medium" type="primary" block @click="handleAssignPermSubmit">
|
||||
确 定
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import RoleAPI from "@/api/system/role";
|
||||
import MenuAPI from "@/api/system/menu";
|
||||
import DaTree from "@/components/da-tree/index.vue";
|
||||
|
||||
let roleId = ref<number>(); // 操作的角色ID
|
||||
const menuPermOptions = ref<OptionType[]>([]); // 所有菜单
|
||||
let rootKeys = ref<number[]>([]); // 根节点数组
|
||||
const DaTreeRef = ref(); // 树引用
|
||||
const showFab = ref(); // 悬浮按钮是否展开
|
||||
|
||||
/**
|
||||
* 分配菜单权限页面数据初始化
|
||||
*/
|
||||
async function initAssignPerm() {
|
||||
// 获取所有的菜单
|
||||
menuPermOptions.value = await MenuAPI.getOptions(false);
|
||||
rootKeys.value = menuPermOptions.value.map((item) => item.value as number);
|
||||
// 回显角色已拥有的菜单
|
||||
RoleAPI.getRoleMenuIds(roleId.value!).then((data) => {
|
||||
const checkedMenuIds = data;
|
||||
if (checkedMenuIds && checkedMenuIds.length > 0) {
|
||||
DaTreeRef.value?.setCheckedKeys(checkedMenuIds, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 展开/收起树节点
|
||||
function doExpandTree(keys: number[] | string, expand: boolean) {
|
||||
DaTreeRef.value?.setExpandedKeys(keys, expand);
|
||||
showFab.value = false;
|
||||
}
|
||||
// 选中/取消树节点
|
||||
function doCheckedTree(keys: number[], checked: boolean) {
|
||||
DaTreeRef.value?.setCheckedKeys(keys, checked);
|
||||
showFab.value = false;
|
||||
}
|
||||
|
||||
// 分配菜单权限提交
|
||||
function handleAssignPermSubmit() {
|
||||
if (roleId.value) {
|
||||
const checkedMenuIds: number[] = DaTreeRef.value?.getCheckedKeys();
|
||||
RoleAPI.updateRoleMenus(roleId.value, checkedMenuIds).then(() => {
|
||||
uni.showToast({ title: "分配权限成功", icon: "success" });
|
||||
uni.navigateBack({ delta: 1 });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onLoad((options: any) => {
|
||||
if (options && options.id) {
|
||||
roleId.value = options.id;
|
||||
}
|
||||
initAssignPerm();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.footer-buttons {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
padding: 20rpx;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
:deep(.custom-button) {
|
||||
box-sizing: border-box;
|
||||
width: 64px !important;
|
||||
min-width: auto !important;
|
||||
height: 32px !important;
|
||||
margin: 8rpx;
|
||||
border-radius: 16px !important;
|
||||
}
|
||||
</style>
|
356
src/pages/work/role/index.vue
Normal file
356
src/pages/work/role/index.vue
Normal file
@ -0,0 +1,356 @@
|
||||
<template>
|
||||
<view class="role">
|
||||
<!-- 筛选 -->
|
||||
<wd-drop-menu>
|
||||
<wd-drop-menu-item ref="filterDropMenu" icon="filter" icon-size="18px" title="筛选">
|
||||
<view>
|
||||
<wd-input
|
||||
v-model="queryParams.keywords"
|
||||
label="关键字"
|
||||
type="text"
|
||||
placeholder="请输入关键字"
|
||||
/>
|
||||
<view class="flex-between py-2">
|
||||
<wd-button custom-class="w-20%" type="info" @click="handleResetQuery">重置</wd-button>
|
||||
<wd-button custom-class="w-70%" @click="handleQuery">确定</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-drop-menu-item>
|
||||
</wd-drop-menu>
|
||||
<!-- 卡片列表 -->
|
||||
<view class="list-container">
|
||||
<wd-card v-for="item in dataList" :key="item.id" class="role-card">
|
||||
<template #title>
|
||||
<view class="flex-between">
|
||||
<view class="flex-center">
|
||||
<view class="ml-2">
|
||||
<view class="font-bold">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view>
|
||||
<wd-tag v-if="item.status === 1" type="success" plain>正常</wd-tag>
|
||||
<wd-tag v-else-if="item.status === 0" plain>停用</wd-tag>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<wd-cell-group>
|
||||
<wd-cell title="编码" title-width="150rpx" :value="item.code" />
|
||||
<wd-cell title="排序号" title-width="150rpx" :value="item.sort" />
|
||||
</wd-cell-group>
|
||||
|
||||
<template #footer>
|
||||
<view class="flex-between">
|
||||
<view class="text-left">
|
||||
<wd-text text="创建时间:" size="small" class="font-bold" />
|
||||
<wd-text :text="item.createTime" size="small" />
|
||||
</view>
|
||||
<view class="flex-right">
|
||||
<wd-button size="small" plain @click="handleAction(item)">操作</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</wd-card>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<wd-loadmore :state="loadMoreState" @reload="queryPageData" />
|
||||
</view>
|
||||
<!-- 底部按钮 -->
|
||||
<wd-fab
|
||||
position="left-bottom"
|
||||
:expandable="false"
|
||||
customStyle="width: 1rem; height: 1rem; line-height: 1rem;z-index:9"
|
||||
@click="handleOpenDialog()"
|
||||
/>
|
||||
|
||||
<wd-popup v-model="dialog.visible" position="bottom" custom-class="yl-popup">
|
||||
<wd-form ref="roleFormRef" :model="formData" :rules="rules">
|
||||
<wd-cell-group border>
|
||||
<wd-input v-model="formData.name" label="角色名称" prop="name" />
|
||||
<wd-input v-model="formData.code" label="角色编码" prop="code" />
|
||||
<wd-select-picker
|
||||
v-model="formData.dataScope"
|
||||
type="radio"
|
||||
label="数据权限"
|
||||
:columns="dataScopeOptions"
|
||||
:align-right="true"
|
||||
prop="dataScope"
|
||||
/>
|
||||
<wd-cell title="状态" prop="status">
|
||||
<wd-switch v-model="formData.status" :active-value="1" :inactive-value="0" />
|
||||
</wd-cell>
|
||||
<wd-cell title="排序" prop="sort">
|
||||
<wd-input-number v-model="formData.sort" />
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
<view class="footer">
|
||||
<wd-button type="primary" block @click="handleSubmit">提交</wd-button>
|
||||
</view>
|
||||
</wd-popup>
|
||||
<wd-message-box />
|
||||
</view>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import RoleAPI, { RolePageVO, RolePageQuery, RoleForm } from "@/api/system/role";
|
||||
import { onLoad, onReachBottom } from "@dcloudio/uni-app";
|
||||
import { LoadMoreState } from "wot-design-uni/components/wd-loadmore/types";
|
||||
import { DropMenuItemExpose } from "wot-design-uni/components/wd-drop-menu-item/types";
|
||||
import { FormInstance } from "wot-design-uni/components/wd-form/types";
|
||||
import { FormRules } from "wot-design-uni/components/wd-form/types";
|
||||
import { useMessage } from "wot-design-uni";
|
||||
const message = useMessage();
|
||||
|
||||
const loadMoreState = ref<LoadMoreState>("loading");
|
||||
|
||||
const total = ref(0);
|
||||
|
||||
const queryParams = reactive<RolePageQuery>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
/**
|
||||
* 搜索栏
|
||||
*/
|
||||
const filterDropMenu = ref<DropMenuItemExpose>();
|
||||
|
||||
function handleQuery() {
|
||||
filterDropMenu.value?.close();
|
||||
queryParams.pageNum = 1;
|
||||
dataList.value = [];
|
||||
queryPageData();
|
||||
}
|
||||
/**
|
||||
* 重置查询
|
||||
*/
|
||||
const handleResetQuery = () => {
|
||||
filterDropMenu.value?.close();
|
||||
queryParams.keywords = "";
|
||||
queryParams.pageNum = 1;
|
||||
dataList.value = [];
|
||||
queryPageData();
|
||||
};
|
||||
|
||||
// 角色列表数据
|
||||
const dataList = ref<RolePageVO[]>([]);
|
||||
|
||||
/**
|
||||
* 查询分页数据
|
||||
*/
|
||||
function queryPageData() {
|
||||
loadMoreState.value = "loading";
|
||||
RoleAPI.getPage(queryParams)
|
||||
.then((data) => {
|
||||
dataList.value?.push(...data.list);
|
||||
total.value = data.total;
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log("系统异常", e);
|
||||
})
|
||||
.finally(() => {
|
||||
loadMoreState.value = "finished";
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 触底事件
|
||||
*/
|
||||
onReachBottom(() => {
|
||||
if (queryParams.pageNum * queryParams.pageSize < total.value) {
|
||||
queryParams.pageNum++;
|
||||
queryPageData();
|
||||
} else {
|
||||
loadMoreState.value = "finished";
|
||||
}
|
||||
});
|
||||
|
||||
// 操作按钮
|
||||
const handleAction = (item: RolePageVO) => {
|
||||
const actions = ["编辑", "分配权限", "删除"];
|
||||
uni.showActionSheet({
|
||||
itemList: actions,
|
||||
success: ({ tapIndex }) => {
|
||||
switch (actions[tapIndex]) {
|
||||
case "编辑":
|
||||
handleOpenDialog(item.id);
|
||||
break;
|
||||
case "分配权限":
|
||||
handleAssignPerm(item.id);
|
||||
break;
|
||||
case "删除":
|
||||
handleDelete(item.id);
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const dialog = reactive({
|
||||
visible: false,
|
||||
});
|
||||
|
||||
const dataScopeOptions = ref<Record<string, any>[]>([
|
||||
{ label: "全部数据", value: 0 },
|
||||
{ label: "部门及子部门数据", value: 1 },
|
||||
{ label: "本部门数据", value: 2 },
|
||||
{ label: "本人数据", value: 3 },
|
||||
]);
|
||||
|
||||
const formData = reactive<RoleForm>({
|
||||
dataScope: 0,
|
||||
sort: 1,
|
||||
});
|
||||
|
||||
const rules: FormRules = {
|
||||
name: [{ required: true, message: "请输入角色名称", trigger: "blur" }],
|
||||
code: [{ required: true, message: "请输入角色编码", trigger: "blur" }],
|
||||
dataScope: [{ required: true, message: "请选择数据权限", trigger: "blur" }],
|
||||
status: [{ required: true, message: "请选择状态", trigger: "change" }],
|
||||
sort: [{ required: true, message: "请输入排序号", trigger: "change" }],
|
||||
};
|
||||
|
||||
const roleFormRef = ref<FormInstance>();
|
||||
|
||||
/**
|
||||
* 打开弹窗
|
||||
*/
|
||||
async function handleOpenDialog(id?: number) {
|
||||
dialog.visible = true;
|
||||
formData.id = undefined;
|
||||
formData.name = "";
|
||||
formData.code = "";
|
||||
formData.dataScope = 0;
|
||||
formData.status = 1;
|
||||
formData.sort = 1;
|
||||
if (id) {
|
||||
RoleAPI.getFormData(id).then((data) => {
|
||||
Object.assign(formData, { ...data });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交保存
|
||||
*/
|
||||
function handleSubmit() {
|
||||
if (roleFormRef.value) {
|
||||
roleFormRef.value.validate().then(({ valid }) => {
|
||||
if (valid) {
|
||||
const roleId = formData.id;
|
||||
if (roleId) {
|
||||
RoleAPI.update(roleId, formData).then(() => {
|
||||
uni.showToast({ title: "修改成功", icon: "success" });
|
||||
dialog.visible = false;
|
||||
queryParams.pageNum = 1;
|
||||
handleQuery();
|
||||
});
|
||||
} else {
|
||||
RoleAPI.add(formData).then(() => {
|
||||
uni.showToast({ title: "添加成功", icon: "success" });
|
||||
dialog.visible = false;
|
||||
queryParams.pageNum = 1;
|
||||
handleQuery();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*
|
||||
* @param id 用户id
|
||||
*/
|
||||
function handleDelete(id: number) {
|
||||
message
|
||||
.confirm({
|
||||
msg: "确认删除角色吗?",
|
||||
title: "提示",
|
||||
})
|
||||
.then(() => {
|
||||
RoleAPI.deleteByIds(id + "").then(() => {
|
||||
uni.showToast({ title: "删除成功", icon: "success" });
|
||||
queryParams.pageNum = 1;
|
||||
handleQuery();
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
console.log("点击了取消按钮");
|
||||
});
|
||||
}
|
||||
|
||||
function handleAssignPerm(id: number) {
|
||||
uni.navigateTo({
|
||||
url: "/pages/work/role/assign-perm?id=" + id,
|
||||
});
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
handleQuery();
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
// https://wot-design-uni.pages.dev/guide/common-problems#%E5%B0%8F%E7%A8%8B%E5%BA%8F%E6%A0%B7%E5%BC%8F%E9%9A%94%E7%A6%BB
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "shared",
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.role {
|
||||
:deep(.wd-drop-menu .wd-drop-menu__item) {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 0 50rpx;
|
||||
}
|
||||
.list-container {
|
||||
.role-card {
|
||||
margin-top: 20rpx;
|
||||
:deep(.wd-cell__wrapper) {
|
||||
padding: 4rpx 0;
|
||||
}
|
||||
:deep(.wd-cell) {
|
||||
padding-right: 10rpx;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(.wd-cell__wrapper) {
|
||||
padding: 4rpx 0;
|
||||
}
|
||||
|
||||
:deep(.wd-cell) {
|
||||
padding-right: 10rpx;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
|
||||
:deep(.wd-fab__trigger) {
|
||||
width: 80rpx !important;
|
||||
height: 80rpx !important;
|
||||
}
|
||||
.yl-popup {
|
||||
.footer {
|
||||
margin: 30rpx 0;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* wot组件直接写class,在小程序上无效
|
||||
* wot组件提供的custom-class属性也不能写unocss的写法
|
||||
* 故wot组件只能使用custom-class属性并在vue文件中自己写样式
|
||||
* 并且custom-class的类名需要使用:deep包裹,否则在小程序上也没有效果
|
||||
*/
|
||||
:deep(.w-20per) {
|
||||
width: 20%;
|
||||
}
|
||||
:deep(.w-70per) {
|
||||
width: 70%;
|
||||
}
|
||||
}
|
||||
</style>
|
426
src/pages/work/user/index.vue
Normal file
426
src/pages/work/user/index.vue
Normal file
@ -0,0 +1,426 @@
|
||||
<template>
|
||||
<!-- 自定义导航栏的占位 -->
|
||||
<view style="width: 100%; height: var(--status-bar-height)" />
|
||||
|
||||
<view class="user-container">
|
||||
<!-- 自定义导航栏 -->
|
||||
<wd-navbar title="用户管理">
|
||||
<template #left>
|
||||
<wd-icon name="thin-arrow-left" @click="handleNavigateback()" />
|
||||
</template>
|
||||
</wd-navbar>
|
||||
|
||||
<!-- 排序筛选 -->
|
||||
<view class="filter-container">
|
||||
<wd-drop-menu>
|
||||
<wd-drop-menu-item
|
||||
v-model="sortValue"
|
||||
:options="sortOptions"
|
||||
title="排序"
|
||||
icon="unfold-more"
|
||||
@change="handleSortChange"
|
||||
/>
|
||||
<wd-drop-menu-item ref="filterDropMenu" title="筛选" icon="filter">
|
||||
<view>
|
||||
<wd-input
|
||||
v-model="queryParams.keywords"
|
||||
label="关键字"
|
||||
placeholder="用户名/昵称/手机号"
|
||||
/>
|
||||
|
||||
<cu-date-query v-model="queryParams.createTime" label="创建时间" />
|
||||
|
||||
<view class="flex-between py-2">
|
||||
<wd-button type="info" @click="handleResetQuery">重置</wd-button>
|
||||
<wd-button @click="handleQuery">查询</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-drop-menu-item>
|
||||
</wd-drop-menu>
|
||||
</view>
|
||||
|
||||
<!-- 数据列表 -->
|
||||
<view class="data-container">
|
||||
<wd-card v-for="item in pageData" :key="item.id">
|
||||
<template #title>
|
||||
<view class="flex-between">
|
||||
<view class="flex-center">
|
||||
<wd-img :width="50" :height="50" round :src="item.avatar" />
|
||||
<view class="ml-2">
|
||||
<view class="font-bold">
|
||||
{{ item.nickname }}
|
||||
<wd-icon v-if="item.gender == 1" name="gender-male" class="color-#4D80F0" />
|
||||
<wd-icon
|
||||
v-else-if="item.gender == 2"
|
||||
name="gender-female"
|
||||
class="color-#FA4350"
|
||||
/>
|
||||
</view>
|
||||
<view class="mt-1"><wd-text :text="item.deptName" size="12px" /></view>
|
||||
</view>
|
||||
</view>
|
||||
<view>
|
||||
<wd-tag v-if="item.status === 1" type="success" plain>正常</wd-tag>
|
||||
<wd-tag v-else-if="item.status === 0" plain>禁用</wd-tag>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<wd-cell-group>
|
||||
<wd-cell title="用户名" :value="item.username" icon="user" />
|
||||
<wd-cell title="角色" :value="item.roleNames" icon="usergroup" />
|
||||
<wd-cell title="手机号码" :value="item.mobile" icon="mobile" />
|
||||
<wd-cell title="邮箱" :value="item.email" icon="mail" />
|
||||
</wd-cell-group>
|
||||
|
||||
<template #footer>
|
||||
<view class="flex-between">
|
||||
<view class="text-left">
|
||||
<wd-text text="创建时间:" size="small" class="font-bold" />
|
||||
<wd-text :text="item.createTime" size="small" />
|
||||
</view>
|
||||
<view class="text-right">
|
||||
<wd-button
|
||||
type="primary"
|
||||
size="small"
|
||||
plain
|
||||
:v-has-perm="'sys:user:edit'"
|
||||
@click="handleOpenDialog(item.id)"
|
||||
>
|
||||
编辑
|
||||
</wd-button>
|
||||
|
||||
<wd-button
|
||||
type="error"
|
||||
size="small"
|
||||
plain
|
||||
:v-has-perm="'sys:user:delete'"
|
||||
@click="handleDelete(item.id)"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</wd-card>
|
||||
|
||||
<wd-loadmore v-if="total > 0" :state="loadMoreState" @reload="loadmore" />
|
||||
<wd-status-tip v-else-if="total == 0" image="search" tip="当前搜索无结果" />
|
||||
</view>
|
||||
|
||||
<!-- 弹窗表单 -->
|
||||
<wd-popup v-model="dialog.visible" position="bottom" @close="hancleCloseDialog">
|
||||
<wd-form ref="userFormRef" :model="formData" :rules="rules">
|
||||
<wd-cell-group border>
|
||||
<wd-input v-model="formData.username" label="用户名" :readonly="!formData.id" required />
|
||||
<wd-input v-model="formData.nickname" label="昵称" required />
|
||||
<wd-select-picker
|
||||
v-model="formData.roleIds"
|
||||
label="角色"
|
||||
:columns="roleOptions"
|
||||
required
|
||||
/>
|
||||
<CuPicker
|
||||
v-model="formData.deptId"
|
||||
v-model:data="deptOptions"
|
||||
label="部门"
|
||||
:required="true"
|
||||
/>
|
||||
<wd-input v-model="formData.mobile" label="手机号" prop="mobile" />
|
||||
<wd-input v-model="formData.email" label="邮箱" prop="email" />
|
||||
<wd-cell title="状态">
|
||||
<wd-switch v-model="formData.status" :active-value="1" :inactive-value="0" required />
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
<view class="popup-footer">
|
||||
<wd-button type="primary" block @click="handleSubmit">提交</wd-button>
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
||||
<!-- 悬浮操作按钮 -->
|
||||
<wd-fab
|
||||
:v-has-perm="'sys:user:add'"
|
||||
position="left-bottom"
|
||||
:expandable="false"
|
||||
custom-style="z-index: 9"
|
||||
@click="handleOpenDialog"
|
||||
/>
|
||||
|
||||
<wd-message-box />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onLoad, onReachBottom } from "@dcloudio/uni-app";
|
||||
|
||||
import { LoadMoreState } from "wot-design-uni/components/wd-loadmore/types";
|
||||
import { FormRules } from "wot-design-uni/components/wd-form/types";
|
||||
import { useMessage } from "wot-design-uni";
|
||||
|
||||
import UserAPI, { type UserPageQuery, UserPageVO, UserForm } from "@/api/system/user";
|
||||
import RoleAPI from "@/api/system/role";
|
||||
import DeptAPI from "@/api/system/dept";
|
||||
|
||||
const message = useMessage();
|
||||
const loadMoreState = ref<LoadMoreState>("loading");
|
||||
const filterDropMenu = ref();
|
||||
const userFormRef = ref();
|
||||
|
||||
const sortValue = ref(0);
|
||||
const sortOptions = ref<Record<string, any>[]>([
|
||||
{ label: "默认排序", value: 0 },
|
||||
{ label: "最近创建", value: 1 },
|
||||
{ label: "最近更新", value: 2 },
|
||||
]);
|
||||
|
||||
let queryParams: UserPageQuery = {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
};
|
||||
|
||||
const total = ref(0);
|
||||
const pageData = ref<UserPageVO[]>([]);
|
||||
|
||||
const dialog = reactive({
|
||||
visible: false,
|
||||
});
|
||||
|
||||
const initialFormData: UserForm = {
|
||||
id: undefined,
|
||||
roleIds: [],
|
||||
username: undefined,
|
||||
nickname: undefined,
|
||||
deptId: undefined,
|
||||
mobile: undefined,
|
||||
email: undefined,
|
||||
status: 1,
|
||||
};
|
||||
|
||||
const formData = reactive<UserForm>({ ...initialFormData });
|
||||
|
||||
const roleOptions = ref<Record<string, any>[]>([]);
|
||||
const deptOptions = ref<OptionType[]>([]);
|
||||
const rules: FormRules = {
|
||||
username: [{ required: true, message: "请输入用户名" }],
|
||||
nickname: [{ required: true, message: "请输入昵称" }],
|
||||
roleIds: [{ required: true, message: "请选择角色" }],
|
||||
deptId: [{ required: true, message: "请选择部门" }],
|
||||
status: [{ required: true, message: "请选择状态" }],
|
||||
mobile: [
|
||||
{
|
||||
required: false,
|
||||
message: "手机号格式不正确",
|
||||
validator: (value) => {
|
||||
if (!value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (!/^1[3456789]\d{9}$/.test(value)) {
|
||||
return Promise.reject("手机号格式不正确");
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
email: [
|
||||
{
|
||||
required: false,
|
||||
message: "邮箱格式不正确",
|
||||
validator: (value) => {
|
||||
if (!value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (!/^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/.test(value)) {
|
||||
return Promise.reject("邮箱格式不正确");
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* 排序改变
|
||||
*/
|
||||
const handleSortChange = ({ value }: { value: number }) => {
|
||||
if (value === 1) {
|
||||
queryParams.field = "create_time";
|
||||
queryParams.direction = "DESC";
|
||||
} else if (value === 2) {
|
||||
queryParams.field = "update_time";
|
||||
queryParams.direction = "DESC";
|
||||
} else {
|
||||
queryParams.field = "";
|
||||
queryParams.direction = "";
|
||||
}
|
||||
handleQuery();
|
||||
};
|
||||
|
||||
/**
|
||||
* 查询
|
||||
*/
|
||||
const handleQuery = () => {
|
||||
filterDropMenu.value?.close();
|
||||
queryParams.pageNum = 1;
|
||||
loadmore();
|
||||
console.log("queryParams", queryParams);
|
||||
};
|
||||
|
||||
/**
|
||||
* 重置查询
|
||||
*/
|
||||
const handleResetQuery = () => {
|
||||
queryParams = {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
};
|
||||
handleQuery();
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载更多
|
||||
*/
|
||||
function loadmore() {
|
||||
loadMoreState.value = "loading";
|
||||
UserAPI.getPage(queryParams)
|
||||
.then((data) => {
|
||||
pageData.value = data.list;
|
||||
total.value = data.total;
|
||||
queryParams.pageNum++;
|
||||
})
|
||||
.catch((e) => {
|
||||
pageData.value = [];
|
||||
})
|
||||
.finally(() => {
|
||||
loadMoreState.value = "finished";
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开弹窗
|
||||
*/
|
||||
async function handleOpenDialog(id?: number) {
|
||||
dialog.visible = true;
|
||||
roleOptions.value = await RoleAPI.getOptions();
|
||||
deptOptions.value = await DeptAPI.getOptions();
|
||||
if (id) {
|
||||
UserAPI.getFormData(id).then((data) => {
|
||||
Object.assign(formData, { ...data });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交保存
|
||||
*/
|
||||
function handleSubmit() {
|
||||
hancleCloseDialog();
|
||||
userFormRef.value.validate().then(({ valid }: { valid: boolean }) => {
|
||||
if (valid) {
|
||||
const userId = formData.id;
|
||||
if (userId) {
|
||||
UserAPI.update(userId, formData).then(() => {
|
||||
message.show("修改成功");
|
||||
hancleCloseDialog();
|
||||
handleQuery();
|
||||
});
|
||||
} else {
|
||||
UserAPI.add(formData).then(() => {
|
||||
message.show("添加成功");
|
||||
hancleCloseDialog();
|
||||
handleQuery();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
function resetForm() {
|
||||
userFormRef.value.reset();
|
||||
Object.assign(formData, initialFormData);
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
function hancleCloseDialog() {
|
||||
dialog.visible = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*
|
||||
* @param id 用户id
|
||||
*/
|
||||
function handleDelete(id: number) {
|
||||
message
|
||||
.confirm({
|
||||
msg: "确认删除用户吗?",
|
||||
title: "提示",
|
||||
})
|
||||
.then(() => {
|
||||
UserAPI.deleteByIds(id + "").then(() => {
|
||||
message.show("删除成功");
|
||||
handleQuery();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回
|
||||
*/
|
||||
function handleNavigateback() {
|
||||
uni.navigateBack();
|
||||
}
|
||||
|
||||
// 触底事件
|
||||
onReachBottom(() => {
|
||||
if (queryParams.pageNum * queryParams.pageSize < total.value) {
|
||||
loadmore();
|
||||
} else if (queryParams.pageNum * queryParams.pageSize >= total.value) {
|
||||
loadMoreState.value = "finished";
|
||||
}
|
||||
});
|
||||
|
||||
onLoad(() => {
|
||||
handleQuery();
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
// https://wot-design-uni.pages.dev/guide/common-problems#%E5%B0%8F%E7%A8%8B%E5%BA%8F%E6%A0%B7%E5%BC%8F%E9%9A%94%E7%A6%BB
|
||||
export default {
|
||||
options: {
|
||||
styleIsolation: "shared",
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user-container {
|
||||
:deep(.wd-cell__wrapper) {
|
||||
padding: 4rpx 0;
|
||||
}
|
||||
|
||||
:deep(.wd-cell) {
|
||||
padding-right: 10rpx;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
|
||||
:deep(.wd-fab__trigger) {
|
||||
width: 80rpx !important;
|
||||
height: 80rpx !important;
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
padding: 10rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.data-container {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user