首页调整

This commit is contained in:
Lilixu007 2026-02-25 18:16:20 +08:00
parent b65017c7a2
commit e19615a89d
11 changed files with 1034 additions and 285 deletions

View File

@ -89,4 +89,13 @@
<style>
/*每个页面公共css */
/* 隐藏底部tabBar */
uni-tabbar {
display: none !important;
}
.uni-tabbar {
display: none !important;
}
</style>

View File

@ -0,0 +1,439 @@
<template>
<view v-if="visible" class="login-modal-overlay">
<view class="login-modal-mask" @tap="onMaskClick"></view>
<view class="login-modal-wrapper">
<view class="login-modal-content">
<view class="login-modal-header">
<text class="login-modal-title">请登录</text>
<text class="login-modal-close" @tap="onClose">×</text>
</view>
<view class="login-modal-body">
<view class="login-form-item">
<text class="login-form-label">账号</text>
<input
class="login-form-input"
type="text"
v-model="username"
placeholder="请输入账号"
:focus="autoFocus"
:adjust-position="true"
confirm-type="next"
@focus="onUsernameFocus"
@blur="onUsernameBlur"
@confirm="focusPassword"
@tap.stop
@click.stop
/>
</view>
<view class="login-form-item">
<text class="login-form-label">密码</text>
<input
class="login-form-input"
type="text"
v-model="password"
:password="true"
placeholder="请输入密码"
:adjust-position="true"
:focus="passwordFocus"
confirm-type="done"
@focus="onPasswordFocus"
@blur="onPasswordBlur"
@confirm="onLogin"
@tap.stop
@click.stop
/>
</view>
<!-- 错误提示 -->
<view v-if="errorMsg" class="login-error-msg">
<text class="error-icon"></text>
<text class="error-text">{{ errorMsg }}</text>
</view>
<!-- 成功提示 -->
<view v-if="successMsg" class="login-success-msg">
<text class="success-icon"></text>
<text class="success-text">{{ successMsg }}</text>
</view>
<button
class="login-modal-btn"
type="primary"
:disabled="loading"
@tap.stop="onLogin"
>
{{ loading ? '登录中...' : '登录' }}
</button>
<!-- 底部提示 -->
<view class="login-modal-footer">
<text class="footer-text">首次登录请联系管理员获取账号</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { setToken } from '../utils/auth'
import { login } from '../api/system/login'
export default {
name: 'LoginModal',
props: {
show: {
type: Boolean,
default: false
},
closable: {
type: Boolean,
default: false
}
},
data() {
return {
visible: false,
username: '',
password: '',
loading: false,
autoFocus: false,
passwordFocus: false,
errorMsg: '',
successMsg: ''
}
},
watch: {
show: {
handler(val) {
console.log('LoginModal show changed:', val)
this.visible = val
if (val) {
//
this.username = uni.getStorageSync('XINLI_LAST_USERNAME') || ''
//
this.errorMsg = ''
this.successMsg = ''
//
this.$nextTick(() => {
setTimeout(() => {
console.log('Setting autoFocus to true')
this.autoFocus = true
}, 600)
})
} else {
this.autoFocus = false
this.passwordFocus = false
this.errorMsg = ''
this.successMsg = ''
}
},
immediate: true
}
},
methods: {
onUsernameFocus() {
console.log('Username input focused')
this.errorMsg = ''
this.successMsg = ''
},
onUsernameBlur() {
console.log('Username input blurred')
},
onPasswordFocus() {
console.log('Password input focused')
this.errorMsg = ''
this.successMsg = ''
},
onPasswordBlur() {
console.log('Password input blurred')
},
focusPassword() {
console.log('Focusing password field')
this.autoFocus = false
this.$nextTick(() => {
this.passwordFocus = true
})
},
onMaskClick() {
console.log('Mask clicked, closable:', this.closable)
if (this.closable) {
this.onClose()
} else {
uni.showToast({
title: '请先登录后才能使用',
icon: 'none'
})
}
},
onClose() {
if (!this.closable) {
uni.showToast({
title: '请先登录后才能使用',
icon: 'none'
})
return
}
this.$emit('close')
},
onLogin() {
console.log('Login button clicked')
if (this.loading) return
const username = (this.username || '').trim()
const password = (this.password || '').trim()
if (!username) {
this.errorMsg = '请输入账号'
return
}
if (!password) {
this.errorMsg = '请输入密码'
return
}
this.errorMsg = ''
this.successMsg = ''
this.loading = true
uni.showLoading({ title: '登录中...' })
login({ username, password })
.then((res) => {
this.loading = false
uni.hideLoading()
const data = res && res.data ? res.data : null
if (!data || data.code !== 200) {
const errorMsg = (data && data.msg) ? data.msg : '登录失败'
this.errorMsg = errorMsg
uni.showToast({
title: errorMsg,
icon: 'none',
duration: 2000
})
return
}
const token = data.token
if (!token) {
this.errorMsg = '登录失败未返回token'
uni.showToast({
title: '登录失败未返回token',
icon: 'none'
})
return
}
setToken(token)
uni.setStorageSync('XINLI_LAST_USERNAME', username)
//
this.successMsg = '登录成功!正在进入系统...'
uni.showToast({
title: '登录成功',
icon: 'success'
})
//
this.password = ''
this.errorMsg = ''
//
this.$emit('success')
//
setTimeout(() => {
this.$emit('close')
//
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
if (currentPage && currentPage.$vm && typeof currentPage.$vm.onShow === 'function') {
currentPage.$vm.onShow()
}
}, 500)
})
.catch((e) => {
this.loading = false
uni.hideLoading()
const errorMsg = e && e.message ? e.message : '网络错误,请检查网络连接'
this.errorMsg = errorMsg
uni.showToast({
title: errorMsg,
icon: 'none',
duration: 2000
})
})
}
}
}
</script>
<style>
/* 不使用scoped确保样式优先级 */
.login-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100vw;
height: 100vh;
z-index: 9999999;
display: flex;
align-items: center;
justify-content: center;
}
.login-modal-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 1;
}
.login-modal-wrapper {
position: relative;
z-index: 2;
width: 600rpx;
max-width: 90vw;
}
.login-modal-content {
background: #ffffff;
border-radius: 24rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.3);
overflow: hidden;
}
.login-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx 32rpx 24rpx;
border-bottom: 1px solid #f0f0f0;
background: #ffffff;
}
.login-modal-title {
font-size: 36rpx;
font-weight: 700;
color: #1f2329;
}
.login-modal-close {
font-size: 48rpx;
color: #8f959e;
line-height: 1;
padding: 0 8rpx;
}
.login-modal-body {
padding: 32rpx;
background: #ffffff;
}
.login-form-item {
margin-bottom: 24rpx;
}
.login-form-label {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #1f2329;
margin-bottom: 12rpx;
}
.login-form-input {
display: block;
width: 100%;
height: 88rpx;
background: #f7f8fa;
border-radius: 12rpx;
padding: 0 20rpx;
font-size: 30rpx;
line-height: 88rpx;
box-sizing: border-box;
border: 2rpx solid transparent;
color: #1f2329;
}
.login-form-input::placeholder {
color: #c0c4cc;
}
.login-error-msg {
display: flex;
align-items: center;
padding: 16rpx 20rpx;
background: #fff2e8;
border-radius: 8rpx;
margin-bottom: 16rpx;
border-left: 4rpx solid #ff7875;
}
.error-icon {
font-size: 32rpx;
margin-right: 12rpx;
}
.error-text {
font-size: 26rpx;
color: #d4380d;
line-height: 1.5;
flex: 1;
}
.login-success-msg {
display: flex;
align-items: center;
padding: 16rpx 20rpx;
background: #f6ffed;
border-radius: 8rpx;
margin-bottom: 16rpx;
border-left: 4rpx solid #52c41a;
}
.success-icon {
font-size: 32rpx;
margin-right: 12rpx;
color: #52c41a;
font-weight: bold;
}
.success-text {
font-size: 26rpx;
color: #389e0d;
line-height: 1.5;
flex: 1;
}
.login-modal-btn {
width: 100%;
margin-top: 16rpx;
border-radius: 12rpx;
font-size: 32rpx;
font-weight: 600;
height: 88rpx;
line-height: 88rpx;
}
.login-modal-footer {
margin-top: 24rpx;
text-align: center;
}
.footer-text {
font-size: 24rpx;
color: #8f959e;
line-height: 1.5;
}
</style>

View File

@ -1,167 +0,0 @@
<template>
<view class="tabbar" :class="{ h5: isH5 }" :style="safeAreaStyle">
<view class="tabbar-item" :class="{ active: active === 'pages/index/index' }" @tap="switchTab('pages/index/index')">
<view class="tabbar-icon">
<uni-icons type="home" size="20" :color="active === 'pages/index/index' ? selectedColor : color"></uni-icons>
</view>
<view class="tabbar-text">面板</view>
</view>
<view class="tabbar-item" :class="{ active: active === 'pages/message/notice' }" @tap="switchTab('pages/message/notice')">
<view class="tabbar-icon">
<uni-icons type="notification" size="20" :color="active === 'pages/message/notice' ? selectedColor : color"></uni-icons>
</view>
<view class="tabbar-text">通知</view>
</view>
<view class="tabbar-item" :class="{ active: active === 'pages/settings/index' }" @tap="switchTab('pages/settings/index')">
<view class="tabbar-icon">
<uni-icons type="person" size="20" :color="active === 'pages/settings/index' ? selectedColor : color"></uni-icons>
</view>
<view class="tabbar-text">我的</view>
</view>
</view>
</template>
<script>
import UniIcons from '@/uni_modules/uni-icons/components/uni-icons/uni-icons.vue'
export default {
components: {
UniIcons
},
data() {
return {
active: 'pages/index/index',
isH5: false,
color: '#646a73',
selectedColor: '#1677ff'
}
},
computed: {
safeAreaStyle() {
try {
const info = uni.getSystemInfoSync()
const bottom = info && info.safeAreaInsets ? info.safeAreaInsets.bottom : 0
return bottom ? `padding-bottom:${bottom}px;` : ''
} catch (e) {
return ''
}
}
},
mounted() {
try {
const info = uni.getSystemInfoSync()
this.isH5 = info && info.uniPlatform === 'web'
} catch (e) {
this.isH5 = false
}
if (this.isH5) {
this.color = 'rgba(201, 242, 255, 0.85)'
this.selectedColor = '#74d8ff'
}
this.updateActive()
},
onShow() {
this.updateActive()
},
methods: {
updateActive() {
try {
const pages = getCurrentPages()
const current = pages && pages.length ? pages[pages.length - 1] : null
let route = current && current.route ? current.route : ''
if (route && route[0] === '/') route = route.slice(1)
this.active = route || this.active
} catch (e) {
// ignore
}
},
switchTab(url) {
this.updateActive()
if (this.active === url) return
this.active = url
uni.switchTab({ url: '/' + url })
setTimeout(() => {
this.updateActive()
}, 50)
}
}
}
</script>
<style>
.tabbar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
height: 56px;
background: #ffffff;
border-top: 1px solid rgba(0, 0, 0, 0.08);
display: flex;
align-items: center;
justify-content: space-around;
box-sizing: content-box;
z-index: 999;
}
.tabbar.h5 {
background: linear-gradient(180deg, rgba(2, 8, 22, 0.92) 0%, rgba(2, 8, 22, 0.78) 100%);
border-top: 1px solid rgba(0, 188, 255, 0.22);
box-shadow: 0 -10px 18px rgba(0, 0, 0, 0.35), 0 0 22px rgba(0, 166, 255, 0.10);
}
.tabbar-item {
flex: 1;
height: 56px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #646a73;
}
.tabbar.h5 .tabbar-item {
color: rgba(201, 242, 255, 0.85);
}
.tabbar-item.active {
color: #1677ff;
}
.tabbar.h5 .tabbar-item.active {
color: #74d8ff;
}
.tabbar-icon {
width: 34px;
height: 28px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 120ms ease;
}
.tabbar-item.active .tabbar-icon {
background: rgba(22, 119, 255, 0.14);
}
.tabbar.h5 .tabbar-item.active .tabbar-icon {
background: rgba(116, 216, 255, 0.14);
box-shadow: 0 0 14px rgba(116, 216, 255, 0.22);
}
.tabbar-item.active .tabbar-text {
color: #1677ff;
}
.tabbar.h5 .tabbar-item.active .tabbar-text {
color: #74d8ff;
}
.tabbar-text {
margin-top: 2px;
font-size: 12px;
line-height: 16px;
}
</style>

6
xinlidsj/package-lock.json generated Normal file
View File

@ -0,0 +1,6 @@
{
"name": "xinlidsj",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View File

@ -190,29 +190,11 @@
"backgroundColor": "#050b18"
},
"tabBar": {
"custom": true,
"color": "#646a73",
"selectedColor": "#1677ff",
"backgroundColor": "#ffffff",
"borderStyle": "black",
"custom": false,
"list": [
{
"pagePath": "pages/index/index",
"text": "面板",
"iconPath": "static/home.png",
"selectedIconPath": "static/home.png"
},
{
"pagePath": "pages/message/notice",
"text": "通知",
"iconPath": "static/notice.png",
"selectedIconPath": "static/notice.png"
},
{
"pagePath": "pages/settings/index",
"text": "我的",
"iconPath": "static/my_light.png",
"selectedIconPath": "static/my_light.png"
"text": "首页"
}
]
},

View File

@ -982,9 +982,9 @@
content: '';
position: absolute;
left: 0;
top: 6rpx;
top: 10rpx;
width: 6rpx;
height: 24rpx;
height: 100rpx;
border-radius: 6rpx;
background: linear-gradient(180deg, rgba(116, 216, 255, 0.95) 0%, rgba(43, 107, 255, 0.85) 100%);
}

View File

@ -1,5 +1,6 @@
<template>
<view class="page" v-if="!isH5">
<!-- 主内容区域 - 登录时禁用交互 -->
<view class="page" v-if="!isH5" :class="{ 'page-disabled': showLoginModal }">
<view class="section">
<view class="section-title">核心功能</view>
<view class="grid">
@ -124,6 +125,9 @@
<view class="big-top-action-item" @tap="goInterventionTasks">
<image class="big-top-action-ico" src="/static/6.png" mode="aspectFit" />
</view>
<view class="big-top-action-item" @tap="goNotice">
<image class="big-top-action-ico" src="/static/7.png" mode="aspectFit" />
</view>
</view>
</view>
@ -231,6 +235,13 @@
<view :id="aiChatBottomId" class="big-ai-bottom"></view>
</scroll-view>
<view class="big-ai-input">
<view
class="big-ai-voice-btn"
:class="{ active: voiceMode }"
@tap="toggleVoiceMode"
>
<uni-icons type="mic-filled" size="20" :color="voiceMode ? '#00f0ff' : '#64748B'"></uni-icons>
</view>
<input
class="big-ai-text"
v-model="aiChatInput"
@ -307,6 +318,9 @@
</view>
</view>
<!-- 登录弹窗 - 放在最后确保在最上层 -->
<LoginModal :show="showLoginModal" :closable="false" @success="onLoginSuccess" @close="showLoginModal = false" />
</template>
<script>
@ -315,21 +329,30 @@
import { getUnreadMessageList, markMessageRead } from '../../api/app/message'
import { openLink, getMessageWsUrl } from '../../utils/link'
import { request } from '../../utils/request'
import { getBaseUrl } from '../../utils/config'
import { getReport, listReport } from '../../api/psychology/report'
import { getStudentOptions, getUserAssessmentSummary } from '../../api/psychology/assessment'
import QiunDataCharts from '../../uni_modules/qiun-data-charts/components/qiun-data-charts/qiun-data-charts.vue'
import UniIcons from '@/uni_modules/uni-icons/components/uni-icons/uni-icons.vue'
import LoginModal from '../../components/LoginModal.vue'
export default {
components: {
QiunDataCharts,
UniIcons
UniIcons,
LoginModal
},
data() {
return {
showLoginModal: false,
socketOpen: false,
connecting: false,
voiceTipsOpen: false,
voiceMode: false,
recorder: null,
mediaRecorder: null,
mediaStream: null,
audioChunks: [],
isH5: false,
centerVideoUrl: '',
aiChatOpen: true,
@ -527,22 +550,21 @@
} catch (e) {
this.isH5 = false
}
if (this.isH5) {
this.fetchCenterVideo()
}
//
const token = getToken()
if (!token) return
this.initMessageChannel()
this.checkUnreadNoticePopup()
this.checkUnreadMessagePopup()
if (this.isH5) {
this.fetchBigData()
this.fetchInboxList()
if (!token) {
this.showLoginModal = true
return
}
this.initApp()
},
onShow() {
//
const token = getToken()
if (!token) return
if (!token) {
this.showLoginModal = true
return
}
this.checkUnreadNoticePopup()
this.checkUnreadMessagePopup()
if (this.isH5) {
@ -550,6 +572,22 @@
}
},
methods: {
initApp() {
if (this.isH5) {
this.fetchCenterVideo()
}
this.initMessageChannel()
this.checkUnreadNoticePopup()
this.checkUnreadMessagePopup()
if (this.isH5) {
this.fetchBigData()
this.fetchInboxList()
}
},
onLoginSuccess() {
//
this.initApp()
},
getBailianConfig() {
const HARDCODED_BAILIAN_API_KEY = 'sk-f991fd13fb044abebeaea81b9848c22b'
let env = null
@ -716,6 +754,255 @@
})
}
},
toggleVoiceMode() {
this.voiceMode = !this.voiceMode
if (this.voiceMode) {
uni.showToast({
title: '语音对话已开启',
icon: 'none',
duration: 1500
})
this.startVoiceRecognition()
} else {
uni.showToast({
title: '语音对话已关闭',
icon: 'none',
duration: 1500
})
this.stopVoiceRecognition()
}
},
startVoiceRecognition() {
// H5使API
// #ifdef H5
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
uni.showToast({
title: '浏览器不支持录音功能',
icon: 'none',
duration: 2000
})
this.voiceMode = false
return
}
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
console.log('获取麦克风权限成功')
this.mediaStream = stream
this.mediaRecorder = new MediaRecorder(stream)
this.audioChunks = []
this.mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
this.audioChunks.push(event.data)
}
}
this.mediaRecorder.onstop = () => {
console.log('录音停止')
const audioBlob = new Blob(this.audioChunks, { type: 'audio/webm' })
this.uploadAudioBlob(audioBlob)
//
if (this.mediaStream) {
this.mediaStream.getTracks().forEach(track => track.stop())
}
}
this.mediaRecorder.start()
console.log('开始录音...')
})
.catch(err => {
console.error('获取麦克风权限失败:', err)
uni.showToast({
title: '无法访问麦克风,请检查权限',
icon: 'none',
duration: 2000
})
this.voiceMode = false
})
// #endif
// #ifndef H5
//
if (typeof uni.getRecorderManager !== 'function') {
uni.showToast({
title: '当前环境不支持录音功能',
icon: 'none',
duration: 2000
})
this.voiceMode = false
return
}
if (!this.recorder) {
this.recorder = uni.getRecorderManager()
this.recorder.onStop((res) => {
console.log('录音停止', res)
this.onVoiceRecorderStop(res)
})
this.recorder.onError((err) => {
console.error('录音错误', err)
this.voiceMode = false
const msg = (err && err.errMsg) ? err.errMsg : '录音失败'
uni.showToast({
title: msg,
icon: 'none'
})
})
this.recorder.onStart(() => {
console.log('录音开始')
})
}
console.log('开始录音...')
this.recorder.start({
duration: 60000,
sampleRate: 16000,
numberOfChannels: 1,
encodeBitRate: 96000,
format: 'mp3'
})
// #endif
},
stopVoiceRecognition() {
// #ifdef H5
if (this.mediaRecorder && this.mediaRecorder.state === 'recording') {
this.mediaRecorder.stop()
}
// #endif
// #ifndef H5
if (this.recorder && this.voiceMode) {
this.recorder.stop()
}
// #endif
},
onVoiceRecorderStop(res) {
if (!res || !res.tempFilePath) {
uni.showToast({
title: '未获取到录音文件',
icon: 'none'
})
return
}
this.uploadAndAsrForAiChat(res.tempFilePath)
},
uploadAndAsrForAiChat(tempFilePath) {
console.log('开始上传音频文件:', tempFilePath)
uni.showLoading({ title: '识别中...' })
const baseUrl = getBaseUrl()
console.log('上传地址:', baseUrl + '/voice/asr')
uni.uploadFile({
url: baseUrl + '/voice/asr',
filePath: tempFilePath,
name: 'file',
formData: { language: 'zh' },
header: {
'Authorization': getToken() || ''
},
success: (res) => {
console.log('上传成功,响应:', res)
uni.hideLoading()
let data = null
try {
data = JSON.parse(res.data)
} catch (e) {
console.error('解析响应失败:', e)
uni.showToast({
title: '服务返回格式错误',
icon: 'none'
})
return
}
console.log('解析后的数据:', data)
if (!data || data.code !== 200) {
uni.showToast({
title: (data && data.msg) ? data.msg : '识别失败',
icon: 'none'
})
return
}
const recognizedText = data.text || ''
if (!recognizedText) {
uni.showToast({
title: '未识别到有效文本',
icon: 'none'
})
return
}
console.log('识别文本:', recognizedText)
//
this.aiChatInput = recognizedText
this.sendAiChat()
},
fail: (e) => {
console.error('上传失败:', e)
uni.hideLoading()
uni.showToast({
title: e && e.errMsg ? e.errMsg : '上传失败',
icon: 'none'
})
}
})
},
uploadAudioBlob(audioBlob) {
console.log('开始上传音频Blob:', audioBlob)
uni.showLoading({ title: '识别中...' })
const baseUrl = getBaseUrl()
const formData = new FormData()
formData.append('file', audioBlob, 'audio.webm')
formData.append('language', 'zh')
fetch(baseUrl + '/voice/asr', {
method: 'POST',
headers: {
'Authorization': getToken() || ''
},
body: formData
})
.then(response => response.json())
.then(data => {
console.log('上传成功,响应:', data)
uni.hideLoading()
if (!data || data.code !== 200) {
uni.showToast({
title: (data && data.msg) ? data.msg : '识别失败',
icon: 'none'
})
return
}
const recognizedText = data.text || ''
if (!recognizedText) {
uni.showToast({
title: '未识别到有效文本',
icon: 'none'
})
return
}
console.log('识别文本:', recognizedText)
this.aiChatInput = recognizedText
this.sendAiChat()
})
.catch(err => {
console.error('上传失败:', err)
uni.hideLoading()
uni.showToast({
title: '上传失败: ' + err.message,
icon: 'none'
})
})
},
scrollAiChatToBottom() {
// scroll-into-view
this.aiChatScrollInto = this.aiChatBottomId + '-' + Date.now()
@ -1627,8 +1914,13 @@
url: '/pages/dashboard/index'
})
},
goNotice() {
uni.navigateTo({
url: '/pages/message/notice'
})
},
goSettings() {
uni.switchTab({
uni.navigateTo({
url: '/pages/settings/index'
})
}
@ -1637,20 +1929,41 @@
</script>
<style scoped>
/* 自适应字体大小变量 - 基于视口宽度 */
:root {
--font-xs: calc(22rpx + 0.3vw); /* 小字体 */
--font-sm: calc(24rpx + 0.35vw); /* 小字体 */
--font-base: calc(26rpx + 0.4vw); /* 基础字体 */
--font-md: calc(28rpx + 0.45vw); /* 中等字体 */
--font-lg: calc(32rpx + 0.6vw); /* 大字体 */
--font-xl: calc(38rpx + 0.75vw); /* 超大字体 */
--font-2xl: calc(56rpx + 1.2vw); /* 特大字体 */
--font-3xl: calc(60rpx + 1.5vw); /* 巨大字体 */
}
.page {
min-height: 100vh;
padding: 24rpx 24rpx 120rpx;
padding: 24rpx 24rpx 0;
box-sizing: border-box;
background: #F4F6FB;
--c-primary: #1677ff;
--c-danger: #E87A7A;
/* 页面级字体变量 */
--font-xs: calc(22rpx + 0.3vw);
--font-sm: calc(24rpx + 0.35vw);
--font-base: calc(26rpx + 0.4vw);
--font-md: calc(28rpx + 0.45vw);
--font-lg: calc(32rpx + 0.6vw);
--font-xl: calc(38rpx + 0.75vw);
--font-2xl: calc(56rpx + 1.2vw);
--font-3xl: calc(60rpx + 1.5vw);
}
.section {
margin-top: 14rpx;
}
.section-title {
font-size: 28rpx;
font-size: var(--font-md);
font-weight: 700;
color: #111827;
margin: 10rpx 6rpx 14rpx;
@ -1739,8 +2052,8 @@
}
.card-icon {
width: 64rpx;
height: 64rpx;
width: calc(64rpx + 1.5vh);
height: calc(64rpx + 1.5vh);
border-radius: 16rpx;
margin-bottom: 16rpx;
display: flex;
@ -1777,14 +2090,14 @@
}
.card-title {
font-size: 32rpx;
font-size: var(--font-lg);
font-weight: 700;
color: #111827;
}
.card-desc {
margin-top: 10rpx;
font-size: 24rpx;
font-size: var(--font-sm);
color: #6B7280;
}
@ -1802,7 +2115,7 @@
justify-content: space-between;
}
.fold-title {
font-size: 24rpx;
font-size: var(--font-sm);
font-weight: 700;
color: #334155;
}
@ -1810,7 +2123,7 @@
padding: 0 14rpx 14rpx;
}
.fold-item {
font-size: 24rpx;
font-size: var(--font-sm);
color: #475569;
line-height: 40rpx;
}
@ -1838,7 +2151,7 @@
transform: scale(0.98);
}
.mini-text {
font-size: 22rpx;
font-size: var(--font-xs);
color: #334155;
}
@ -1847,8 +2160,8 @@
}
.page.big {
min-height: 100vh;
padding: 18rpx 18rpx 24rpx;
height: 100vh;
padding: 12rpx 12rpx 0;
box-sizing: border-box;
position: relative;
overflow: hidden;
@ -1859,14 +2172,14 @@
--glass-2: rgba(10, 18, 38, 0.52);
background: #020610;
color: rgba(242, 252, 255, 0.96);
font-size: 24rpx;
font-size: var(--font-sm);
line-height: 1.5;
text-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.55);
}
.big-center-media {
width: 100%;
height: calc(74vh - 360rpx);
max-height: 720rpx;
max-height: calc(720rpx + 7vh);
border-radius: 18rpx;
overflow: hidden;
}
@ -1898,7 +2211,7 @@
padding: 16rpx 18rpx;
}
.big-center-text-title {
font-size: 26rpx;
font-size: var(--font-base);
font-weight: 900;
color: rgba(220, 250, 255, 0.94);
letter-spacing: 1rpx;
@ -1906,16 +2219,16 @@
}
.big-center-text-desc {
margin-top: 10rpx;
font-size: 22rpx;
font-size: var(--font-xs);
line-height: 32rpx;
color: rgba(201, 242, 255, 0.72);
}
.big-panel-inbox {
margin-top: 16rpx;
min-height: 320rpx;
min-height: calc(320rpx + 2vh);
}
.big-panel-portrait .big-chart {
height: 420rpx;
height: calc(420rpx + 3vh);
}
.big-inbox-list {
display: flex;
@ -1930,16 +2243,16 @@
box-sizing: border-box;
}
.big-inbox-title {
font-size: 24rpx;
font-size: var(--font-sm);
font-weight: 900;
color: rgba(242, 252, 255, 0.92);
}
.big-inbox-desc {
margin-top: 8rpx;
font-size: 22rpx;
font-size: var(--font-xs);
color: rgba(242, 252, 255, 0.84);
line-height: 28rpx;
max-height: 56rpx;
max-height: calc(56rpx + 0.5vh);
overflow: hidden;
}
.page.big:before {
@ -1996,11 +2309,11 @@
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 60rpx;
font-size: var(--font-3xl);
font-weight: 900;
letter-spacing: 2rpx;
color: rgba(242, 252, 255, 0.98);
min-height: 120rpx;
min-height: calc(120rpx + 1.2vh);
width: 100%;
padding: 0;
box-sizing: border-box;
@ -2025,9 +2338,10 @@
z-index: 3;
padding: 0 40rpx;
box-sizing: border-box;
font-size: calc(48rpx + 0.9vw);
}
.big-title {
min-height: 120rpx;
min-height: calc(120rpx + 1.2vh);
width: 100%;
padding: 0;
}
@ -2048,12 +2362,12 @@
}
.big-top-action-row {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 12rpx;
align-items: center;
}
.big-top-action-item {
height: 140rpx;
height: calc(140rpx + 1.5vh);
display: flex;
align-items: center;
justify-content: center;
@ -2063,14 +2377,14 @@
box-sizing: border-box;
}
.big-top-action-ico {
width: 124rpx;
height: 124rpx;
width: calc(124rpx + 1.2vh);
height: calc(124rpx + 1.2vh);
filter: drop-shadow(0 0 14rpx rgba(0, 240, 255, 0.26)) drop-shadow(0 0 26rpx rgba(60, 140, 255, 0.18));
}
.big-grid {
margin-top: 14rpx;
display: flex;
gap: 22rpx;
gap: 12rpx;
width: 100%;
max-width: none;
margin-left: 0;
@ -2078,14 +2392,14 @@
justify-content: space-between;
}
.big-col {
width: 28%;
width: 27%;
display: flex;
flex-direction: column;
gap: 16rpx;
}
.big-center {
flex: 0 0 44%;
width: 44%;
flex: 0 0 46%;
width: 46%;
min-width: 0;
display: flex;
flex-direction: column;
@ -2094,8 +2408,8 @@
.big-panel {
position: relative;
border-radius: 18rpx;
padding: 26rpx 26rpx 22rpx;
min-height: 320rpx;
padding: 18rpx 18rpx 16rpx;
min-height: calc(320rpx + 2vh);
border: 1px solid rgba(0, 240, 255, 0.16);
background-image:
linear-gradient(135deg, rgba(0, 240, 255, 0.08) 0%, rgba(60, 140, 255, 0.05) 35%, rgba(165, 90, 255, 0.06) 100%),
@ -2107,18 +2421,18 @@
overflow: hidden;
}
.big-panel-core {
min-height: 460rpx;
min-height: calc(460rpx + 3vh);
}
.big-panel-core .big-ring {
height: 440rpx;
height: calc(440rpx + 3vh);
}
.big-panel-core .big-ring:before {
width: 440rpx;
height: 440rpx;
width: calc(440rpx + 3vh);
height: calc(440rpx + 3vh);
}
.big-panel-core .big-ring-center {
width: 250rpx;
height: 250rpx;
width: calc(250rpx + 2vh);
height: calc(250rpx + 2vh);
}
.big-metrics {
position: relative;
@ -2134,7 +2448,7 @@
min-width: 340rpx;
}
.big-metric-label {
font-size: 24rpx;
font-size: var(--font-sm);
font-weight: 700;
color: rgba(220, 250, 255, 0.78);
letter-spacing: 1rpx;
@ -2142,7 +2456,7 @@
}
.big-metric-value {
display: block;
font-size: 56rpx;
font-size: var(--font-2xl);
font-weight: 900;
line-height: 1;
color: rgba(242, 252, 255, 0.96);
@ -2218,7 +2532,7 @@
opacity: 0.95;
}
.big-panel-title {
font-size: 32rpx;
font-size: var(--font-lg);
font-weight: 900;
color: rgba(242, 252, 255, 0.94);
padding-left: 14rpx;
@ -2259,8 +2573,9 @@
}
.big-panel-report .big-panel-title:before {
left: 10rpx;
top: 10rpx;
height: 22rpx;
top: 50%;
transform: translateY(-50%);
height: calc(22rpx + 0.8vh);
background: linear-gradient(180deg, rgba(0, 240, 255, 0.95) 0%, rgba(39, 120, 255, 0.85) 55%, rgba(165, 90, 255, 0.85) 100%);
box-shadow: 0 0 14rpx rgba(0, 240, 255, 0.18);
}
@ -2282,14 +2597,15 @@
content: '';
position: absolute;
left: 0;
top: 6rpx;
top: 50%;
transform: translateY(-50%);
width: 6rpx;
height: 24rpx;
height: calc(24rpx + 0.8vh);
border-radius: 6rpx;
background: linear-gradient(180deg, rgba(116, 216, 255, 0.95) 0%, rgba(43, 107, 255, 0.85) 100%);
}
.big-panel-sub {
font-size: 24rpx;
font-size: var(--font-sm);
color: rgba(242, 252, 255, 0.84);
font-weight: 800;
}
@ -2307,7 +2623,7 @@
.big-panel-clear {
padding: 6rpx 12rpx;
border-radius: 12rpx;
font-size: 22rpx;
font-size: var(--font-xs);
font-weight: 900;
color: rgba(242, 252, 255, 0.86);
border: 1px solid rgba(116, 216, 255, 0.20);
@ -2331,7 +2647,7 @@
z-index: 1;
}
.big-ai-messages {
height: 640rpx;
height: calc(640rpx + 4vh);
padding: 8rpx 10rpx;
box-sizing: border-box;
border-radius: 14rpx;
@ -2340,7 +2656,7 @@
}
.big-ai-empty {
padding: 16rpx 10rpx;
font-size: 22rpx;
font-size: var(--font-xs);
color: rgba(242, 252, 255, 0.62);
font-weight: 700;
line-height: 1.4;
@ -2359,7 +2675,7 @@
max-width: 78%;
padding: 12rpx 14rpx;
border-radius: 14rpx;
font-size: 24rpx;
font-size: var(--font-sm);
line-height: 1.45;
color: rgba(242, 252, 255, 0.92);
border: 1px solid rgba(0, 240, 255, 0.16);
@ -2380,25 +2696,41 @@
align-items: center;
gap: 10rpx;
}
.big-ai-voice-btn {
width: calc(72rpx + 2vh);
height: calc(72rpx + 2vh);
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(116, 216, 255, 0.18);
background: rgba(7, 13, 28, 0.78);
transition: all 0.3s ease;
}
.big-ai-voice-btn.active {
border-color: rgba(0, 240, 255, 0.6);
background: rgba(0, 240, 255, 0.12);
box-shadow: 0 0 20rpx rgba(0, 240, 255, 0.4);
}
.big-ai-text {
flex: 1;
height: 72rpx;
height: calc(72rpx + 2vh);
border-radius: 14rpx;
border: 1px solid rgba(116, 216, 255, 0.18);
background: rgba(7, 13, 28, 0.78);
padding: 0 14rpx;
font-size: 24rpx;
font-size: var(--font-sm);
color: rgba(242, 252, 255, 0.92);
box-sizing: border-box;
}
.big-ai-send {
width: 120rpx;
height: 72rpx;
height: calc(72rpx + 2vh);
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
font-size: var(--font-sm);
font-weight: 900;
color: rgba(242, 252, 255, 0.92);
border: 1px solid rgba(0, 240, 255, 0.22);
@ -2411,7 +2743,7 @@
.big-ring {
position: relative;
z-index: 1;
height: 360rpx;
height: calc(360rpx + 2.5vh);
}
.big-ring:before {
content: '';
@ -2419,8 +2751,8 @@
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 360rpx;
height: 360rpx;
width: calc(360rpx + 2.5vh);
height: calc(360rpx + 2.5vh);
border-radius: 999rpx;
pointer-events: none;
z-index: 0;
@ -2441,8 +2773,8 @@
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 210rpx;
height: 210rpx;
width: calc(210rpx + 5vh);
height: calc(210rpx + 5vh);
border-radius: 999rpx;
background: rgba(7, 13, 28, 0.72);
z-index: 5;
@ -2454,12 +2786,12 @@
box-sizing: border-box;
}
.big-ring-center-main {
font-size: 34rpx;
font-size: var(--font-lg);
font-weight: 900;
color: rgba(242, 252, 255, 0.96);
}
.big-ring-center-sub {
font-size: 22rpx;
font-size: var(--font-xs);
color: rgba(242, 252, 255, 0.84);
text-align: center;
line-height: 26rpx;
@ -2468,7 +2800,7 @@
.big-chart {
position: relative;
z-index: 1;
height: 340rpx;
height: calc(340rpx + 2.5vh);
}
.big-portrait-cloud {
position: relative;
@ -2510,7 +2842,7 @@
100% { transform: translate(-50%, -50%) translate(0, 0); }
}
.big-chart-xl {
height: 400rpx;
height: calc(400rpx + 3vh);
}
.big-kpis {
position: relative;
@ -2533,19 +2865,19 @@
gap: 10rpx;
}
.big-kpi-ico {
width: 34rpx;
height: 34rpx;
width: calc(34rpx + 1vh);
height: calc(34rpx + 1vh);
filter: drop-shadow(0 0 10rpx rgba(0, 200, 255, 0.22));
}
.big-kpi-num {
font-size: 38rpx;
font-size: var(--font-xl);
font-weight: 900;
color: #74d8ff;
text-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.65);
}
.big-kpi-lab {
margin-top: 6rpx;
font-size: 24rpx;
font-size: var(--font-sm);
color: rgba(242, 252, 255, 0.84);
font-weight: 800;
}
@ -2563,12 +2895,12 @@
padding: 10rpx 2rpx 2rpx;
}
.big-panel-actions {
min-height: 380rpx;
min-height: calc(380rpx + 3vh);
}
.big-action-item {
position: relative;
z-index: 1;
height: 150rpx;
height: calc(150rpx + 1.5vh);
display: flex;
flex-direction: column;
align-items: center;
@ -2584,13 +2916,13 @@
}
.big-action-item:active { transform: scale(0.98); }
.big-action-ico {
width: 68rpx;
height: 68rpx;
width: calc(68rpx + 2vh);
height: calc(68rpx + 2vh);
flex: 0 0 auto;
filter: drop-shadow(0 0 10rpx rgba(0, 200, 255, 0.22));
}
.big-action-text {
font-size: 26rpx;
font-size: var(--font-base);
font-weight: 900;
color: rgba(242, 252, 255, 0.92);
line-height: 1.1;
@ -2613,8 +2945,8 @@
gap: 10rpx;
}
.big-tool-icon {
width: 50rpx;
height: 50rpx;
width: calc(50rpx + 1.5vh);
height: calc(50rpx + 1.5vh);
border-radius: 12rpx;
border: 1px solid rgba(116, 216, 255, 0.22);
background: rgba(10, 18, 38, 0.55);
@ -2623,7 +2955,7 @@
justify-content: center;
}
.big-tool-text {
font-size: 24rpx;
font-size: var(--font-sm);
font-weight: 900;
color: rgba(242, 252, 255, 0.92);
}
@ -2642,8 +2974,8 @@
padding: 10rpx 0;
}
.big-hex {
width: 96rpx;
height: 86rpx;
width: calc(96rpx + 3vh);
height: calc(86rpx + 2.7vh);
clip-path: polygon(25% 6.7%, 75% 6.7%, 100% 50%, 75% 93.3%, 25% 93.3%, 0% 50%);
border: 1px solid rgba(116, 216, 255, 0.28);
background: radial-gradient(circle at 50% 30%, rgba(116, 216, 255, 0.22) 0%, rgba(10, 18, 38, 0.55) 70%);
@ -2653,7 +2985,7 @@
box-shadow: 0 0 18rpx rgba(116, 216, 255, 0.18);
}
.big-nav-text {
font-size: 22rpx;
font-size: var(--font-xs);
font-weight: 900;
color: rgba(242, 252, 255, 0.86);
}
@ -2667,4 +2999,10 @@
width: 100%;
}
}
/* 登录弹窗显示时禁用页面交互 */
.page-disabled {
pointer-events: none;
user-select: none;
}
</style>

View File

@ -1,8 +1,13 @@
<template>
<view class="page" :class="{ big: isH5 }">
<view v-if="isH5" class="big-top">
<view class="big-title">标签筛选</view>
<view class="big-sub">按标签快速定位重点人员</view>
<view class="big-back" @tap="goBack">
<uni-icons type="back" size="24" color="rgba(220, 250, 255, 0.95)"></uni-icons>
</view>
<view class="big-top-content">
<view class="big-title">标签筛选</view>
<view class="big-sub">按标签快速定位重点人员</view>
</view>
</view>
<view class="card">
@ -81,6 +86,11 @@
}
},
methods: {
goBack() {
uni.navigateBack({
delta: 1
})
},
fetchTagOptions() {
this.tagLoading = true
const defaultOptions = [
@ -208,11 +218,36 @@
background: linear-gradient(90deg, rgba(2, 8, 22, 0.86) 0%, rgba(2, 8, 22, 0.40) 50%, rgba(2, 8, 22, 0.86) 100%);
box-shadow: 0 10rpx 22rpx rgba(0, 0, 0, 0.35), 0 0 24rpx rgba(0, 166, 255, 0.14);
display: flex;
flex-direction: column;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 6rpx;
margin-bottom: 18rpx;
position: relative;
}
.page.big .big-back {
position: absolute;
left: 20rpx;
top: 50%;
transform: translateY(-50%);
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12rpx;
background: rgba(0, 188, 255, 0.12);
border: 1px solid rgba(0, 188, 255, 0.22);
cursor: pointer;
}
.page.big .big-back:active {
background: rgba(0, 188, 255, 0.20);
}
.page.big .big-top-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 6rpx;
}
.page.big .big-title {
font-size: 34rpx;

BIN
xinlidsj/static/7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

View File

@ -11,3 +11,4 @@ export function setToken(token) {
export function clearToken() {
uni.removeStorageSync(STORAGE_KEYS.token)
}

View File

@ -0,0 +1,106 @@
# 登录弹窗实现说明
## 功能概述
实现了用户访问系统时,如果未登录则自动弹出登录弹窗,要求用户登录后才能使用系统功能。
## 实现方式
### 1. 创建登录弹窗组件
**文件**: `xinlidsj/components/LoginModal.vue`
**功能特点**:
- 美观的弹窗UI设计
- 支持账号密码登录
- 登录状态加载提示
- 登录成功后自动关闭弹窗并刷新页面
- 可配置是否允许关闭(`closable`属性)
- 自动记住上次登录的账号
**使用方法**:
```vue
<LoginModal
:show="showLoginModal"
:closable="false"
@success="onLoginSuccess"
@close="showLoginModal = false"
/>
```
**Props**:
- `show`: Boolean - 是否显示弹窗
- `closable`: Boolean - 是否允许关闭弹窗默认false强制登录
**Events**:
- `success`: 登录成功时触发
- `close`: 关闭弹窗时触发
### 2. 首页集成登录弹窗
**文件**: `xinlidsj/pages/index/index.vue`
**修改内容**:
1. 引入 `LoginModal` 组件
2. 添加 `showLoginModal` 数据属性
3. 在 `onLoad``onShow` 生命周期中检查登录状态
4. 未登录时显示登录弹窗
5. 登录成功后初始化应用数据
**核心逻辑**:
```javascript
onLoad() {
// 检查登录状态
const token = getToken()
if (!token) {
this.showLoginModal = true // 显示登录弹窗
return
}
this.initApp() // 已登录,初始化应用
}
```
### 3. 登录成功处理
登录成功后的流程:
1. 保存token到本地存储
2. 触发 `success` 事件
3. 父组件调用 `onLoginSuccess()` 方法
4. 初始化应用数据(加载视频、消息、数据等)
5. 自动关闭登录弹窗
6. 刷新当前页面
## 用户体验
### 未登录状态
- 访问首页时自动弹出登录弹窗
- 弹窗遮罩层阻止用户操作页面内容
- 不允许关闭弹窗(`closable=false`),必须登录
- 点击遮罩层会提示"请先登录后才能使用"
### 登录过程
- 输入账号密码
- 点击登录按钮或按回车键提交
- 显示"登录中..."加载状态
- 登录失败显示错误提示
- 登录成功显示成功提示并自动关闭弹窗
### 已登录状态
- 直接进入系统,不显示登录弹窗
- 正常使用所有功能
## 样式特点
- 现代化的弹窗设计
- 半透明黑色遮罩层
- 圆角卡片式弹窗
- 输入框聚焦时高亮效果
- 响应式布局,适配不同屏幕尺寸
## 扩展性
如果需要在其他页面也使用登录弹窗,只需:
1. 引入 `LoginModal` 组件
2. 添加 `showLoginModal` 数据属性
3. 在页面加载时检查登录状态
4. 根据需要设置 `closable` 属性
## 注意事项
1. 登录弹窗使用 `z-index: 9999` 确保在最上层显示
2. 弹窗不可关闭时,点击遮罩层会有友好提示
3. 登录成功后会自动刷新当前页面数据
4. 密码输入框使用 `password` 类型,确保安全性