首页、登录页、引导页等页面

This commit is contained in:
zhouyanli
2026-04-23 17:30:38 +08:00
parent ebf9558713
commit 1252708a8e
50 changed files with 3375 additions and 194 deletions

View File

@@ -0,0 +1,23 @@
import request,{get,post} from "./request.js"
// =======账号密码登录========
export const getLoginPassword = (data) =>{
return post('/api/resident/auth/loginByPassword',data)
}
// 注册新用户获取验证码
export const getSendCode = (data) =>{
return post('/api/resident/auth/sendSmsCode',data)
}
// 注册新用户
export const getRegisterUser = (data) =>{
return post('/api/resident/auth/register',data)
}
// 短信验证码登录
export const getLoginBySms = (data) =>{
return post('/api/resident/auth/loginBySms',data)
}
// 忘记密码接口
export const getResetPassword = (data) =>{
return post('/api/resident/auth/resetPassword',data)
}

View File

@@ -0,0 +1,81 @@
let baseUrl = '';
if (process.env.NODE_ENV === 'development') {
baseUrl = 'http://192.168.0.224:8080'; // 开发环境
} else {
baseUrl = 'http://192.168.1.224:8080'; // 生产环境
}
export default function request(url, data = {}, method = "GET") {
return new Promise((resolve, reject) => {
// 加载中提示(提升用户体验)
uni.showLoading({ title: '请求中...', mask: true });
uni.request({
url: baseUrl + url,
data: data,
method: method,
header: {
"Authorization": "Bearer " + (uni.getStorageSync("token") || ''),
"clientid": "resident",
'content-type': 'application/json'
},
// header: {
// "token": uni.getStorageSync("token") || '',
// 'content-type': 'application/json'
// },
success: (res) => {
uni.hideLoading(); // 关闭加载提示
// 处理HTTP状态码(网络层面)
if (res.statusCode !== 200) {
const errMsg = `请求失败(${res.statusCode})`;
uni.showToast({ title: errMsg, icon: "none", duration: 2000 });
reject(errMsg);
return;
}
// 处理业务状态码(后端返回的data里)
const resData = res.data || {}; // 兼容后端返回空的情况
// 优先处理token过期(401)
if (resData.code === 401) {
uni.showToast({ title: '登录过期,请重新登录', icon: 'none', duration: 2000 });
uni.clearStorageSync('token');
setTimeout(() => {
uni.reLaunch({ url: '/pageSubPack/loginSub/pwd-login' });
}, 1500);
reject('token 过期');
return;
}
// 业务成功(code=200)
if (resData.code === 200) {
resolve(resData); // 只返回核心数据,简化页面调用
}
// 业务失败(code=500)
else if (resData.code === 500) {
const errMsg = resData.msg || '请求失败';
uni.showToast({ title: errMsg, icon: "none", duration: 2000 });
// resolve(null)
reject(errMsg);
}
else {
const errMsg = resData.msg || '业务异常';
uni.showToast({ title: errMsg, icon: "none", duration: 2000 });
reject(errMsg);
}
},
fail: (err) => {
uni.hideLoading(); // 关闭加载提示
const errMsg = err.msg || '网络请求失败';
uni.showToast({ title: errMsg, icon: "none", duration: 2000 });
reject(err);
}
});
});
}
export const get = (url, data) => request(url, data, 'GET');
export const post = (url, data) => request(url, data, 'POST');

View File

@@ -0,0 +1,197 @@
<template>
<view class="reset-pwd-page">
<view class="status_bar"></view>
<!-- 顶部导航栏 -->
<view class="nav-bar">
<uni-icons type="left" size="28" color="#333" @click="goBack"></uni-icons>
<view class="nav-title">修改密码</view>
</view>
<!-- 新密码输入框 -->
<view class="input-item">
<uni-easyinput class="input-content" type="password" v-model="newPassword"
placeholder="请输入密码" :inputBorder="false" :placeholderStyle="placeholderStyle">
</uni-easyinput>
</view>
<!-- 确认密码输入框 -->
<view class="input-item">
<uni-easyinput class="input-content" type="password" v-model="confirmPassword"
placeholder="请再次输入密码" :inputBorder="false" :placeholderStyle="placeholderStyle">
</uni-easyinput>
</view>
<!-- 确定按钮 -->
<button
class="confirm-btn"
:disabled="!canConfirm"
@click="handleConfirm"
>
确定
</button>
</view>
</template>
<script setup>
import { ref, computed } from 'vue';
import {onLoad,onShow} from '@dcloudio/uni-app'
import{getResetPassword} from '../api/api.js'
// ========== 响应式数据 ==========
// 新密码
const newPassword = ref('');
// 确认密码
const confirmPassword = ref('');
const placeholderStyle = ref('font-size:24rpx')
// 定义接收上一页传来的参数
const phone = ref('');
const smsCode = ref('');
// ========== 计算属性 ==========
// 是否可点击确定按钮(密码格式正确 + 两次输入一致)
const canConfirm = computed(() => {
// 密码格式规则:6-16位任意字符(可根据需求调整)
const pwdReg = /^(?=.*[a-zA-Z])(?=.*\d).{6,16}$/;
// 校验条件:新密码符合格式 + 确认密码和新密码一致 + 均不为空
return pwdReg.test(newPassword.value)
&& newPassword.value === confirmPassword.value
&& newPassword.value && confirmPassword.value;
});
// ========== 方法 ==========
// 返回上一页
const goBack = () => {
uni.navigateBack();
};
onShow(() => {
// 从缓存读取参数
const params = uni.getStorageSync('tempChangePwdParams') || {};
// 赋值
phone.value = params.phoneNumber
smsCode.value = params.verifyCode
// 用完删除缓存(避免下次跳转读取旧值)
uni.removeStorageSync('tempChangePwdParams');
});
// 检查表单状态(实时更新按钮状态)
const checkForm = () => {};
// 处理确定按钮点击(修改密码逻辑)
const handleConfirm = () => {
if (!canConfirm.value) return
let params = {
phone:phone.value,
smsCode:smsCode.value,
newPassword:newPassword.value,
confirmPassword:confirmPassword.value,
scene:'reset'
}
getResetPassword(params).then(res =>{
if(res.code === 200){
//uni.setStorageSync('token', res.token); // 假设后端返回token字段
uni.showToast({ title: '修改成功', icon: 'success' });
setTimeout(() =>{
uni.navigateTo({ url: '/pageSubPack/loginSub/pwd-login' });
},1500)
}else{
uni.showToast({ title: "修改失败", icon: 'error' });
}
}).catch(err =>{
uni.showToast({
title: '网络异常',
icon: 'error'
});
})
// 模拟修改密码请求
// uni.showLoading({
// title: '修改中...'
// });
// setTimeout(() => {
// uni.hideLoading();
// uni.showToast({
// title: '密码修改成功',
// icon: 'success'
// });
// // 密码修改成功后返回上一页(或跳转到登录页)
// uni.navigateTo({
// url:"/pages/login/pwd-login"
// });
// }, 1500);
};
</script>
<style scoped>
/* 页面容器 */
.reset-pwd-page {
padding: 20rpx 40rpx 40rpx;
min-height: 100vh;
background: repeating-linear-gradient(180deg, #E2F0FF, #F6FAFF );
}
.status_bar{
width: 100%;
height: 46px;
}
/* 顶部导航栏 */
.nav-bar {
display: flex;
align-items: center;
height: 88rpx;
margin-bottom: 60rpx;
}
.nav-title {
flex: 1;
text-align: center;
font-size: 36rpx;
font-weight: 500;
color: #333;
}
/* 输入框通用样式 */
.input-item {
background-color: #fff;
border-radius: 40rpx;
padding: 0 30rpx;
height: 88rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
display: flex;
align-items: center;
}
/* 输入框内容 */
.input-content {
flex: 1;
font-size: 32rpx;
color: #333;
height: 100%;
}
/* 确定按钮 */
.confirm-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background-color: #4D5DFA;
color: #fff;
border-radius: 44rpx;
font-size: 32rpx;
margin-top: 20rpx;
}
/* 确定按钮禁用状态 */
.confirm-btn:disabled {
background-color: #99c8ff;
color: #fff;
}
/* 禁用按钮边框 */
button:after{
border-radius: 50px;
}
</style>

View File

@@ -0,0 +1,229 @@
<template>
<view class="forget-page">
<view class="status_bar"></view>
<view class="nav-bar">
<uni-icons type="left" size="28" color="#333" @click="goBack"></uni-icons>
<view class="nav-title">忘记密码</view>
</view>
<!-- 手机号输入框 -->
<view class="input-item">
<view class="input-icon">
<image src="http://47.104.199.163:9090/images/login/phone.png" mode="" style="height: 100%;width: 100%;"></image>
</view>
<uni-easyinput class="input-content" type="number" v-model="phoneNumber" trim="all"
placeholder="请输入手机号" :inputBorder="false" maxlength="11" :placeholderStyle="placeholderStyle">
</uni-easyinput>
</view>
<!-- 验证码输入 + 获取验证码按钮 -->
<view class="input-item verify-item">
<view class="input-icon">
<image src="http://47.104.199.163:9090/images/login/code.png" mode="" style="height: 100%;width: 100%;"></image>
<!-- <uni-icons type="locked" size="24" color="#999"></uni-icons> -->
</view>
<uni-easyinput class="input-content verify-input" type="number" v-model="verifyCode" trim="all"
placeholder="请输入验证码" :inputBorder="false" maxlength="6" :placeholderStyle="placeholderStyle">
</uni-easyinput>
<button
class="get-verify-btn"
:disabled="!canGetVerify || countDown > 0"
@click="getVerifyCode"
>
{{ countDown > 0 ? `${countDown}s后重新获取` : '获取验证码' }}
</button>
</view>
<!-- 下一步 -->
<button
class="forget-btn"
:disabled="!canRegister"
@click="handleNext"
>
下一步
</button>
</view>
</template>
<script setup>
import { ref, computed, onUnmounted } from 'vue';
import {getSendCode} from '../api/api.js'
// ====响应式数据======
const phoneNumber = ref('')
const verifyCode = ref('')
const countDown = ref(0)
let timer = null;
const placeholderStyle = ref('font-size:24rpx')
// ========== 计算属性 ==========
// 是否可获取验证码(手机号格式正确)
const canGetVerify = computed(() => {
const phoneReg = /^1[3-9]\d{9}$/;
return phoneReg.test(phoneNumber.value);
});
// 获取验证码
const getVerifyCode = () => {
if (!canGetVerify.value) return;
let params = {
phone:phoneNumber.value,
scene:'reset'
}
getSendCode(params).then(res =>{
if(res.code === 200){
uni.showToast({
title: '验证码已发送',
icon: 'success'
});
// 开始60秒倒计时
countDown.value = 60;
timer = setInterval(() => {
countDown.value--;
if (countDown.value <= 0) {
clearInterval(timer);
timer = null;
}
}, 1000);
}else{
uni.showToast({
title: '验证码发送失败',
icon: 'error'
});
}
})
};
// 是否下一步
const canRegister = computed(() => {
const phoneReg = /^1[3-9]\d{9}$/; // 手机号11位
const verifyReg = /^\d{6}$/; // 验证码6位
return phoneReg.test(phoneNumber.value)
&& verifyReg.test(verifyCode.value)
});
// 下一步
const handleNext = () =>{
// 把参数存到本地缓存
uni.setStorageSync('tempChangePwdParams', {
phoneNumber:phoneNumber.value,
verifyCode: verifyCode.value
});
if(!canRegister.value){
uni.showToast({ title: '请输入',icon: 'error'});
return
}
uni.navigateTo({
url: '/pageSubPack/loginSub/change-pwd'
});
}
// 返回上一页
const goBack = () => {
uni.navigateBack();
};
</script>
<style scoped>
.forget-page{
padding: 20rpx 40rpx 40rpx;
min-height: 100vh;
background: repeating-linear-gradient(180deg, #E2F0FF, #F6FAFF );
}
.status_bar{
width: 100%;
height: 46px;
}
/* 顶部导航栏 */
.nav-bar {
display: flex;
align-items: center;
/* height: 88rpx; */
margin-bottom: 70rpx;
}
.nav-title {
flex: 1;
text-align: center;
font-size: 36rpx;
font-weight: 500;
color: #333;
}
/* 输入框通用样式 */
.input-item {
display: flex;
align-items: center;
background-color: #fff;
border-radius: 40rpx;
padding: 0 30rpx;
height: 88rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
/* 验证码输入项特殊布局 */
.verify-item {
position: relative;
padding-right: 220rpx;
}
/* 输入框图标 */
.input-icon {
margin-right: 20rpx;
color: #999;
width: 50rpx;
height: 50rpx;
}
/* 输入框内容 */
.input-content {
flex: 1;
font-size: 32rpx;
color: #333;
height: 100%;
}
.verify-input {
padding-right: 20rpx;
}
/* 获取验证码按钮 */
.get-verify-btn {
position: absolute;
right: 10rpx;
top: 50%;
transform: translateY(-50%);
width: 200rpx;
height: 70rpx;
line-height: 70rpx;
background-color: #4D5DFA;
color: #fff;
border-radius: 35rpx;
font-size: 28rpx;
padding: 0;
margin: 0;
}
.get-verify-btn:disabled {
background-color: #ccc;
color: #fff;
}
/* 下一步按钮 */
.forget-btn{
width: 100%;
height: 88rpx;
line-height: 88rpx;
background-color: #4D5DFA;
color: #fff;
border-radius: 44rpx;
font-size: 32rpx;
margin-bottom: 40rpx;
}
.forget-btn:disabled{
background-color: #99c8ff;
color: #fff;
}
/* 禁用按钮边框 */
button:after{
border-radius: 50px;
}
</style>

View File

@@ -0,0 +1,359 @@
<template>
<view class="login-page">
<view class="status_bar"></view>
<!-- 登录标题 -->
<view class="login-title">手机验证码登录</view>
<!-- 手机号输入框 -->
<view class="input-item">
<view class="input-icon">
<!-- <uni-icons type="phone" size="24" color="#999"></uni-icons> -->
<image src="http://47.104.199.163:9090/images/login/phone.png" mode="" style="height: 100%;width: 100%;"></image>
</view>
<uni-easyinput class="input-content" type="number" v-model="phoneNumber"
placeholder="请输入手机号" :inputBorder="false" maxlength="11" :placeholderStyle="placeholderStyle">
</uni-easyinput>
<!-- <input
class="input-content"
type="number"
placeholder="请输入手机号"
v-model="phoneNumber"
maxlength="11"
@input="checkForm"
/> -->
</view>
<!-- 验证码输入框 + 获取验证码按钮 -->
<view class="input-item verify-item">
<view class="input-icon">
<image src="http://47.104.199.163:9090/images/login/code.png" mode="" style="height: 100%;width: 100%;"></image>
</view>
<uni-easyinput class="input-content verify-input" type="number" v-model="verifyCode"
placeholder="请输入验证码" :inputBorder="false" maxlength="6" :placeholderStyle="placeholderStyle">
</uni-easyinput>
<!-- <input
class="input-content verify-input"
type="number"
placeholder="请输入验证码"
v-model="verifyCode"
maxlength="6"
@input="checkForm"
/> -->
<button
class="get-verify-btn"
:disabled="!canGetVerify || countDown > 0"
@click="getVerifyCode"
>
{{ countDown > 0 ? `${countDown}s后重新获取` : '获取验证码' }}
</button>
</view>
<!-- 协议勾选 -->
<view class="agreement-item" @click="toggleAgreement">
<uni-icons
type="checkbox-filled"
size="24"
color="#4D5DFA"
v-if="agreeAgreement"
></uni-icons>
<uni-icons
type="checkbox"
size="24"
color="#999"
v-else
></uni-icons>
<text class="agreement-text">我已阅读并同意《用户隐私政策》</text>
</view>
<!-- 登录按钮 -->
<button
class="login-btn"
:disabled="!canLogin"
@click="handleLogin"
>
登录
</button>
<!-- 底部链接 -->
<view class="bottom-link">
<navigator class="link-item" url="/pageSubPack/loginSub/pwd-login">账号密码登录</navigator>
<navigator class="link-item" url="/pageSubPack/loginSub/register-new-user">注册新用户</navigator>
</view>
</view>
</template>
<script setup>
import { ref, computed, onUnmounted } from 'vue';
import {getLoginBySms,getSendCode} from '../api/api.js'
// ========== 响应式数据 ==========
// 手机号
const phoneNumber = ref('');
// 验证码
const verifyCode = ref('');
const placeholderStyle = ref('font-size:24rpx')
// 是否同意协议
const agreeAgreement = ref(true); // 默认勾选
// 倒计时(秒)
const countDown = ref(0);
// 定时器
let timer = null;
// ========== 计算属性 ==========
// 是否可以获取验证码(手机号格式正确)
const canGetVerify = computed(() => {
// 手机号正则:11位数字,以1开头
const phoneReg = /^1[3-9]\d{9}$/;
return phoneReg.test(phoneNumber.value);
});
// 是否可以登录(手机号+验证码+协议都满足)
const canLogin = computed(() => {
const phoneReg = /^1[3-9]\d{9}$/;
const verifyReg = /^\d{6}$/;
return phoneReg.test(phoneNumber.value) && verifyReg.test(verifyCode.value) && agreeAgreement.value;
});
// ========== 方法 ==========
// 检查表单状态
const checkForm = () => {
// 无需额外逻辑,计算属性已实时监听
};
// 切换协议勾选状态
const toggleAgreement = () => {
agreeAgreement.value = !agreeAgreement.value;
};
// 获取验证码
const getVerifyCode = () => {
if (!canGetVerify.value) return;
let params = {
phone:phoneNumber.value,
scene:'login'
}
uni.showLoading({ title: '发送中...', mask: true });
getSendCode(params).then(res =>{
if(res.code === 200){
uni.hideLoading();
uni.showToast({
title: '验证码已发送',
icon: 'success'
});
// 开始60秒倒计时
countDown.value = 60;
timer = setInterval(() => {
countDown.value--;
if (countDown.value <= 0) {
clearInterval(timer);
timer = null;
}
}, 1000);
}else{
uni.showToast({
title: '验证码发送失败',
icon: 'error'
});
}
}).catch(err => {
uni.showToast({
title: '网络异常',
icon: 'error'
});
})
};
// 登录处理
const handleLogin = () => {
if (!canLogin.value) return;
uni.showLoading({ title: '登录中...', mask: true });
let params = {
phone:phoneNumber.value,
smsCode:'123456',
scene:'login',
}
getLoginBySms(params).then(res =>{
if(res.code === 200){
if(res.data){
uni.setStorageSync('token', res.data.token);
// 存用户ID
if(res.data.userId) {
uni.setStorageSync('userId', res.data.userId);
}
// 存当前绑定的小区ID,0 表示未绑定
const currentVillageId = res.data.currentVillageId || 0;
uni.setStorageSync('currentVillageId', currentVillageId);
uni.hideLoading();
uni.showToast({ title: '登录成功', icon: 'success' });
setTimeout(() =>{
uni.switchTab({ url: '/pages/index/index' });
},1500)
}else{
uni.showToast({ title: '登录失败', icon: 'error' });
}
}else{
uni.showToast({ title:'登录失败', icon: 'error' });
}
}).catch(err => {
uni.showToast({
title: '网络异常,请重试',
icon: 'error'
});
})
// 模拟登录请求
// uni.showLoading({
// title: '登录中...'
// });
};
// ========== 生命周期 ==========
// 页面卸载时清除定时器
onUnmounted(() => {
if (timer) {
clearInterval(timer);
}
});
</script>
<style scoped>
/* 页面容器 */
.login-page {
padding: 80rpx 40rpx 40rpx;
/* background-color: #f8f9fa; */
min-height: 100vh;
background: repeating-linear-gradient(to bottom, #E2F0FF, #F6FAFF );
}
.status_bar {
height: 160px;
width: 100%;
}
/* 登录标题 */
.login-title {
font-size: 48rpx;
font-weight: 600;
color: #333;
text-align: center;
margin-bottom: 80rpx;
}
/* 输入框项 */
.input-item {
display: flex;
align-items: center;
background-color: #fff;
border-radius: 40rpx;
padding: 0 30rpx;
height: 88rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
/* 验证码输入项(特殊布局) */
.verify-item {
position: relative;
padding-right: 220rpx;
}
/* 输入框图标 */
.input-icon {
width: 50rpx;
height: 50rpx;
margin-right: 20rpx;
color: #999;
}
/* 输入框内容 */
.input-content {
flex: 1;
font-size: 32rpx;
color: #333;
height: 100%;
}
/* 验证码输入框 */
.verify-input {
padding-right: 20rpx;
}
/* 获取验证码按钮 */
.get-verify-btn {
position: absolute;
right: 10rpx;
top: 50%;
transform: translateY(-50%);
width: 200rpx;
height: 70rpx;
line-height: 70rpx;
background-color: #4D5DFA;
color: #fff;
border-radius: 35rpx;
font-size: 28rpx;
padding: 0;
margin: 0;
}
/* 获取验证码按钮禁用状态 */
.get-verify-btn:disabled {
background-color: #ccc;
color: #fff;
border-radius: 50px;
}
/* 协议勾选项 */
.agreement-item {
display: flex;
align-items: center;
margin: 40rpx 0;
padding-left: 10rpx;
}
/* 协议文本 */
.agreement-text {
font-size: 28rpx;
color: #666;
margin-left: 10rpx;
}
/* 登录按钮 */
.login-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background-color: #4D5DFA;
color: #fff;
border-radius: 44rpx;
font-size: 32rpx;
margin-bottom: 60rpx;
}
/* 登录按钮禁用状态 */
.login-btn:disabled {
background-color: #99c8ff;
color: #fff;
/* border-radius: 50px; */
}
/* 禁用按钮边框 */
button:after{
border-radius: 50px;
}
/* 底部链接 */
.bottom-link {
display: flex;
justify-content: space-between;
padding: 0 20rpx;
}
/* 链接项 */
.link-item {
font-size: 28rpx;
color: #4D5DFA;
text-decoration: none;
}
</style>

View File

@@ -0,0 +1,263 @@
<template>
<view class="login-page">
<view class="status_bar"></view>
<!-- 标题 -->
<view class="login-title">账号密码登录</view>
<!-- 手机号输入框 -->
<view class="input-item">
<view class="input-icon">
<!-- <uni-icons type="phone" size="24" color="#999"></uni-icons> -->
<image src="http://47.104.199.163:9090/images/login/phone.png" mode="" style="height: 100%;width: 100%;"></image>
</view>
<uni-easyinput class="input-content" type="text" v-model="phoneNumber"
placeholder="请输入帐号" :inputBorder="false" :placeholderStyle="placeholderStyle">
</uni-easyinput>
</view>
<!-- 密码输入框 -->
<view class="input-item">
<view class="input-icon">
<!-- <uni-icons type="locked" size="24" color="#999"></uni-icons> -->
<image src="http://47.104.199.163:9090/images/login/password.png" mode="" style="height: 100%;width: 100%;"></image>
</view>
<uni-easyinput class="input-content" type="password" v-model="password"
placeholder="请输入密码" :inputBorder="false" :placeholderStyle="placeholderStyle">
</uni-easyinput>
</view>
<!-- 协议勾选 + 忘记密码 -->
<view class="agreement-wrap">
<view class="agreement-item" @click="toggleAgreement">
<uni-icons
type="checkbox-filled"
size="24"
color="#4D5DFA"
v-if="agreeAgreement"
></uni-icons>
<uni-icons
type="checkbox"
size="24"
color="#999"
v-else
></uni-icons>
<text class="agreement-text">我已阅读并同意《用户隐私政策》</text>
</view>
<navigator class="forget-pwd" url="/pageSubPack/loginSub/forget-pwd">忘记密码?</navigator>
</view>
<!-- 登录按钮 -->
<button
class="login-btn"
:disabled="!canLogin"
@click="handleLogin"
>
登录
</button>
<!-- 底部链接 -->
<view class="bottom-link">
<navigator class="link-item" url="/pageSubPack/loginSub/login">验证码登录</navigator>
<navigator class="link-item" url="/pageSubPack/loginSub/register-new-user">注册新用户</navigator>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue';
import {onLoad} from "@dcloudio/uni-app"
import {getLoginPassword} from '../api/api.js'
// 响应式数据
const phoneNumber = ref('');
const password = ref('');
const agreeAgreement = ref(true); // 默认勾选
const placeholderStyle = ref('font-size:24rpx')
// 计算属性:是否可登录(帐号+密码+协议都满足)
const canLogin = computed(() => {
const phoneReg = /^[a-zA-Z0-9_-]{4,16}$/; // 4-16位
const pwdReg = /^.{6,16}$/; // 密码6-16位
return phoneReg.test(phoneNumber.value) && pwdReg.test(password.value) && agreeAgreement.value;
});
// 切换协议勾选
const toggleAgreement = () => {
agreeAgreement.value = !agreeAgreement.value;
};
// 检查表单
const checkForm = () => {};
onLoad(() =>{
});
// 登录逻辑
const handleLogin = () => {
// if (!canLogin.value) {
// uni.showToast({ title: '请输入账号和密码', icon: 'none' });
// return
// }
// if (!phoneNumber.value) {
// uni.showToast({ title: '请输入账号', icon: 'none' });
// return;
// }
// if (!password.value) {
// uni.showToast({ title: '请输入密码', icon: 'none' });
// return;
// }
// if(!agreeAgreement.value){
// uni.showToast({ title: '请勾选协议', icon: 'none' });
// return;
// }
// uni.showLoading({ title: '登录中...', mask: true });
// let params = {
// username:phoneNumber.value,
// password:password.value
// }
// getLoginPassword(params).then(res =>{
// console.log('33333',res)
// if(res.code === 200){
// uni.setStorageSync('token', res.data.token);
// // 存用户ID
// if(res.data) {
// // 存当前绑定的小区ID,0 表示未绑定
// const currentVillageId = res.data.currentVillageId || 0;
// uni.setStorageSync('currentVillageId', currentVillageId);
// }
// uni.hideLoading();
// uni.showToast({ title: '登录成功', icon: 'success' });
// setTimeout(() =>{
// uni.switchTab({
// url: '/pages/index/index'
// },1000);
// })
// }else{
// uni.showToast({ title: res.msg, icon: 'error' });
// }
// }).catch(err => {
// uni.showToast({
// title: '网络异常',
// icon: 'error'
// });
// })
// 测试
uni.showLoading({
title: '登录中...',
mask:true
});
setTimeout(() => {
uni.switchTab({ url: '/pages/index/index' });
}, 1500);
uni.hideLoading()
};
</script>
<style scoped>
.login-page {
padding: 80rpx 40rpx 40rpx;
/* background-color: #f8f9fa; */
min-height: 100vh;
background: repeating-linear-gradient(180deg, #E2F0FF, #F6FAFF );
}
.status_bar {
height: 160px;
width: 100%;
}
.login-title {
font-size: 48rpx;
font-weight: 600;
color: #333;
text-align: center;
margin-bottom: 80rpx;
}
.input-item {
display: flex;
align-items: center;
background-color: #fff;
border-radius: 40rpx;
padding: 0 30rpx;
height: 88rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.input-icon {
margin-right: 20rpx;
width: 50rpx;
height: 50rpx;
}
.input-content {
flex: 1;
font-size: 28rpx;
color: #333;
height: 100%;
}
.agreement-wrap {
display: flex;
justify-content: space-between;
align-items: center;
margin: 40rpx 0;
padding: 0 10rpx;
}
.agreement-item {
display: flex;
align-items: center;
}
.agreement-text {
font-size: 28rpx;
color: #666;
margin-left: 10rpx;
}
.forget-pwd {
font-size: 28rpx;
color: #4D5DFA;
text-decoration: none;
}
.login-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background-color: #4D5DFA;
color: #fff;
border-radius: 44rpx;
font-size: 32rpx;
margin-bottom: 60rpx;
}
.login-btn:disabled {
background-color: #99c8ff;
color: #fff;
/* border-radius: 50px; */
}
/* 禁用按钮边框 */
button:after{
border-radius: 50px;
}
.bottom-link {
display: flex;
justify-content: space-between;
padding: 0 20rpx;
}
.link-item {
font-size: 28rpx;
color: #4D5DFA;
text-decoration: none;
}
</style>

View File

@@ -0,0 +1,383 @@
<template>
<view class="register-page">
<view class="status_bar"></view>
<!-- 顶部导航 -->
<view class="nav-bar">
<uni-icons type="left" size="28" color="#333" @click="goBack"></uni-icons>
<view class="nav-title">注册</view>
</view>
<!-- 注册标题 -->
<view class="register-title">注册用户</view>
<!-- 手机号输入框 -->
<view class="input-item">
<view class="input-icon">
<image src="http://47.104.199.163:9090/images/login/phone.png" mode="" style="height: 100%;width: 100%;"></image>
</view>
<uni-easyinput class="input-content" type="number" v-model="phoneNumber"
placeholder="请输入手机号" :inputBorder="false" maxlength="11" :placeholderStyle="placeholderStyle">
</uni-easyinput>
</view>
<!-- 验证码输入 + 获取验证码按钮 -->
<view class="input-item verify-item">
<view class="input-icon">
<image src="http://47.104.199.163:9090/images/login/code.png" mode="" style="height: 100%;width: 100%;"></image>
<!-- <uni-icons type="locked" size="24" color="#999"></uni-icons> -->
</view>
<uni-easyinput class="input-content verify-input" type="number" v-model="verifyCode"
placeholder="请输入验证码" :inputBorder="false" maxlength="6" :placeholderStyle="placeholderStyle">
</uni-easyinput>
<button
class="get-verify-btn"
:disabled="!canGetVerify || countDown > 0"
@click="getVerifyCode"
>
{{ countDown > 0 ? `${countDown}s后重新获取` : '获取验证码' }}
</button>
</view>
<!-- 账号输入框 -->
<view class="input-item">
<view class="input-icon">
<image src="http://47.104.199.163:9090/images/login/account.png" mode="" style="height: 100%;width: 100%;"></image>
<!-- <uni-icons type="person" size="24" color="#999"></uni-icons> -->
</view>
<uni-easyinput class="input-content" type="text" v-model="account"
placeholder="请输入账号" :inputBorder="false" :placeholderStyle="placeholderStyle">
</uni-easyinput>
</view>
<!-- 密码输入框 -->
<view class="input-item">
<view class="input-icon">
<image src="http://47.104.199.163:9090/images/login/password.png" mode="" style="height: 100%;width: 100%;"></image>
<!-- <uni-icons type="locked" size="24" color="#999"></uni-icons> -->
</view>
<uni-easyinput class="input-content" type="password" v-model="password"
placeholder="请输入密码" :inputBorder="false" :placeholderStyle="placeholderStyle" >
</uni-easyinput>
</view>
<!-- 协议勾选 -->
<view class="agreement-item" @click="toggleAgreement">
<uni-icons
type="checkbox-filled"
size="24"
color="#4D5DFA"
v-if="agreeAgreement"
></uni-icons>
<uni-icons
type="checkbox"
size="24"
color="#999"
v-else
></uni-icons>
<text class="agreement-text">我已阅读并同意《用户隐私政策》</text>
</view>
<!-- 立即注册按钮 -->
<button
class="register-btn"
:disabled="!canRegister"
@click="handleRegister"
>
立即注册
</button>
<!-- 跳转登录链接 -->
<view class="login-link">
<navigator class="link-item" url="/pageSubPack/loginSub/login">已有账号,去登录</navigator>
</view>
</view>
</template>
<script setup>
import { ref, computed, onUnmounted } from 'vue';
import {getRegisterUser,getSendCode} from '../api/api.js'
// ========== 响应式数据 ==========
// 表单数据
const phoneNumber = ref('');
const verifyCode = ref('');
const account = ref('');
const password = ref('');
const placeholderStyle = ref('font-size:24rpx')
// 协议勾选(默认勾选,和截图一致)
const agreeAgreement = ref(true);
// 验证码倒计时
const countDown = ref(0);
let timer = null;
// ========== 计算属性 ==========
// 是否可获取验证码(手机号格式正确)
const canGetVerify = computed(() => {
const phoneReg = /^1[3-9]\d{9}$/;
return phoneReg.test(phoneNumber.value);
});
// 是否可注册(所有表单项+协议都满足)
const canRegister = computed(() => {
const phoneReg = /^1[3-9]\d{9}$/; // 手机号11位
const verifyReg = /^\d{6}$/; // 验证码6位
const accountReg = /^.{4,20}$/; // 账号4-20位
const pwdReg = /^.{6,16}$/; // 密码6-16位
return phoneReg.test(phoneNumber.value)
&& verifyReg.test(verifyCode.value)
&& accountReg.test(account.value)
&& pwdReg.test(password.value)
&& agreeAgreement.value;
});
// ========== 方法 ==========
// 返回上一页
const goBack = () => {
uni.navigateBack();
};
// 切换协议勾选状态
const toggleAgreement = () => {
agreeAgreement.value = !agreeAgreement.value;
};
// 检查表单状态(实时更新按钮状态)
const checkForm = () => {};
// 获取验证码
const getVerifyCode = () => {
if (!canGetVerify.value) return;
let params = {
phone:phoneNumber.value,
scene:'register'
}
getSendCode(params).then(res =>{
if(res.code === 200){
uni.showToast({
title: '验证码已发送',
icon: 'success'
});
// 开始60秒倒计时
countDown.value = 60;
timer = setInterval(() => {
countDown.value--;
if (countDown.value <= 0) {
clearInterval(timer);
timer = null;
}
}, 1000);
}else{
uni.showToast({
title: '验证码发送失败',
icon: 'error'
});
}
}).catch(err =>{
uni.showToast({
title: '网路异常',
icon: 'error'
});
})
};
// 处理注册逻辑
const handleRegister = () => {
if (!canRegister.value) return;
uni.showLoading({ title: '注册中...', mask: true });
let params = {
phone:phoneNumber.value,
smsCode:verifyCode.value,
username:account.value,
password:password.value
}
getRegisterUser(params).then(res =>{
if(res.code === 200){
uni.hideLoading();
uni.showToast({
title: '注册成功',
icon: 'success'
});
setTimeout(() => {
uni.redirectTo({ url: '/pageSubPack/loginSub/pwd-login' });
}, 1500);
}else{
uni.showToast({
title: '注册失败',
icon: 'error'
});
}
}).catch((err) =>{
uni.showToast({
title: '网络异常',
icon: 'error'
});
})
// 注册成功后跳转到登录页
// uni.redirectTo({
// url: '/pages/login/pwd-login'
// });
// setTimeout(() => {
// }, 1500);
};
// ========== 生命周期 ==========
// 页面卸载时清除定时器
onUnmounted(() => {
if (timer) {
clearInterval(timer);
}
});
</script>
<style scoped>
/* 页面容器 */
.register-page {
padding: 20rpx 40rpx 40rpx;
/* background-color: #f8f9fa; */
min-height: 100vh;
background: repeating-linear-gradient(180deg, #E2F0FF, #F6FAFF );
}
.status_bar {
height: 46px;
width: 100%;
}
/* 顶部导航栏 */
.nav-bar {
display: flex;
align-items: center;
/* height: 88rpx; */
margin-bottom: 70rpx;
}
.nav-title {
flex: 1;
text-align: center;
font-size: 36rpx;
font-weight: 500;
color: #333;
}
/* 注册标题 */
.register-title {
font-size: 48rpx;
font-weight: 600;
color: #333;
margin-bottom: 60rpx;
}
/* 输入框通用样式 */
.input-item {
display: flex;
align-items: center;
background-color: #fff;
border-radius: 40rpx;
padding: 0 30rpx;
height: 88rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
/* 验证码输入项特殊布局 */
.verify-item {
position: relative;
padding-right: 220rpx;
}
/* 输入框图标 */
.input-icon {
margin-right: 20rpx;
color: #999;
width: 50rpx;
height: 50rpx;
}
/* 输入框内容 */
.input-content {
flex: 1;
font-size: 32rpx;
color: #333;
height: 100%;
}
.verify-input {
padding-right: 20rpx;
}
/* 获取验证码按钮 */
.get-verify-btn {
position: absolute;
right: 10rpx;
top: 50%;
transform: translateY(-50%);
width: 200rpx;
height: 70rpx;
line-height: 70rpx;
background-color: #4D5DFA;
color: #fff;
border-radius: 35rpx;
font-size: 28rpx;
padding: 0;
margin: 0;
}
.get-verify-btn:disabled {
background-color: #ccc;
color: #fff;
}
/* 协议勾选项 */
.agreement-item {
display: flex;
align-items: center;
margin: 40rpx 0;
padding-left: 10rpx;
}
.agreement-text {
font-size: 28rpx;
color: #666;
margin-left: 10rpx;
}
/* 立即注册按钮 */
.register-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background-color: #4D5DFA;
color: #fff;
border-radius: 44rpx;
font-size: 32rpx;
margin-bottom: 40rpx;
}
button:after{
border-radius: 50px;
}
.register-btn:disabled {
background-color: #99c8ff;
color: #fff;
}
/* 跳转登录链接 */
.login-link {
text-align: center;
}
.link-item {
font-size: 28rpx;
color: #4D5DFA;
text-decoration: none;
}
</style>

View File

@@ -0,0 +1,133 @@
<template>
<view class="news-detail-page">
<!-- 单条资讯卡片 -->
<view class="news-card">
<!-- 标题 + 右上角未读红点 -->
<view class="news-header">
<text class="news-title">以实干担当不断开创中国特色大国外交实干担当不断开创中国特色大国外交</text>
<view class="unread-dot"></view>
</view>
<!-- 发布人 + 时间 -->
<view class="news-meta">
<text class="author">张三</text>
<text class="publish-time">2026-07-01</text>
</view>
<!-- 内容区域:浅背景卡片 -->
<view class="news-content">
<text class="content-text">
学习贯彻习近平新时代中国特色社会主义思想主题教育开展以来,外交外事战线认真学习贯彻习近平总书记重要讲话精神和党中央部署要求,全面准确把...
</text>
</view>
<!-- 详情按钮 -->
<view class="detail-btn" @click="goToDetail">
<text class="detail-text">详情</text>
<uni-icons type="right" size="14" color="#4D5DFA"></uni-icons>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
// 跳转到详情页方法
const goToDetail = () => {
uni.navigateTo({
url: '/pageSubPack/notice-list/noticeListDetails'
})
}
</script>
<style lang="scss" scoped>
// 页面基础样式
.news-detail-page {
padding: 20rpx;
background-color: #f9f9f9;
min-height: 100vh;
box-sizing: border-box;
}
// 资讯卡片:白色背景 + 圆角
.news-card {
background-color: #ffffff;
border-radius: 12rpx;
padding: 30rpx;
box-sizing: border-box;
}
// 标题行:标题 + 红点
.news-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20rpx;
}
// 资讯标题
.news-title {
font-size: 32rpx;
font-weight: bold;
color: #333333;
line-height: 1.4;
flex: 1;
margin-right: 10rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
// 右上角未读红点
.unread-dot {
width: 8rpx;
height: 8rpx;
background-color: #ff3b30;
border-radius: 50%;
margin-top: 6rpx;
}
// 发布人+时间行
.news-meta {
margin-bottom: 20rpx;
}
.author {
font-size: 26rpx;
color: #666666;
margin-right: 20rpx;
}
.publish-time {
font-size: 26rpx;
color: #999999;
}
// 内容区域:浅背景卡片
.news-content {
background-color: #f5f5f5;
border-radius: 8rpx;
padding: 20rpx;
margin-bottom: 20rpx;
}
.content-text {
font-size: 28rpx;
color: #333333;
line-height: 1.5;
}
// 详情按钮:蓝色文字 + 右箭头
.detail-btn {
display: flex;
align-items: center;
justify-content: space-between;
}
.detail-text {
font-size: 28rpx;
color: #4D5DFA;
margin-right: 8rpx;
}
</style>

View File

@@ -0,0 +1,531 @@
<template>
<view class="container">
<!-- 内容区域 -->
<!-- <scroll-view class="content-scroll" scroll-y> -->
<!-- 文章标题 -->
<view class="article-header">
<view class="article-title">
{{noticeDetail.noticeTitle}}
</view>
</view>
<!-- 作者和日期 -->
<view class="notice-meta">
<view class="notice-author">
<text class="author-name">{{noticeDetail.author}}</text>
<text class="separator">·</text>
<text class="publish-time">{{noticeDetail.publishTime}}</text>
</view>
</view>
<!-- 文章内容 -->
<view class="article-content">
<view class="paragraph">
{{noticeDetail.noticeContent}}
</view>
<view class="articleimg">
<image src="http://47.104.199.163:9090/images/home/notice-1.png" mode="" style="width: 100%;height: 100%;border-radius: 20rpx;"></image>
<image src="http://47.104.199.163:9090/images/home/notice-2.png" mode="" style="width: 100%;height: 100%;border-radius: 20rpx;"></image>
</view>
</view>
<!-- 附件区域 -->
<!-- <view class="attachments-section">
<view class="section-title">
<view class="title-text">相关附件</view>
<view class="title-line"></view>
</view>
<view class="attachments-list"> -->
<!-- DOC文件 -->
<!-- <view class="attachment-item" @click="downloadFile('filename.DOC')">
<view class="attachment-left">
<view class="file-icon doc-icon">📄</view>
<view class="file-info">
<view class="file-name">文件名.DOC</view>
<view class="file-size">299kb</view>
</view>
</view>
<view class="download-btn">
<view class="download-icon">↓</view>
</view>
</view> -->
<!-- PDF文件 -->
<!-- <view class="attachment-item" @click="downloadFile('filename.PDF')">
<view class="attachment-left">
<view class="file-icon pdf-icon">📄</view>
<view class="file-info">
<view class="file-name">文件名.PDF</view>
<view class="file-size">299kb</view>
</view>
</view>
<view class="download-btn">
<view class="download-icon">↓</view>
</view>
</view>
</view>
</view> -->
<!-- </scroll-view> -->
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
// import {getNoticeDetails} from '../api/api.js'
const isLoading = ref(false)
const noticeDetail = ref({})
const currentNoticeId = ref('') // 接收公告跳转的id
// =====方法====
onLoad((options) =>{
currentNoticeId.value = options?.id
// console.log('currentNoticeId',currentNoticeId.value)
if(currentNoticeId.value){
getDetailsData(currentNoticeId.value)
}
// console.log('currentNoticeId',currentNoticeId.value)
})
// 获取详情数据
const getDetailsData = async (noticeId) =>{
try{
const res = await getNoticeDetails(noticeId)
if(res.code === 200){
noticeDetail.value = res.data || {}
uni.$emit('noticeRead',{
noticeId:noticeId,
readStatus:noticeDetail.value.readStatus
})
}
}catch(err){
uni.showToast({
title:"加载失败",
icon:"none"
})
}
}
// 下载文件
const downloadFile = (fileName) => {
uni.showModal({
title: '下载确认',
content: `确定要下载 "${fileName}" 吗?`,
success: (res) => {
if (res.confirm) {
isLoading.value = true
// 显示下载进度
uni.showLoading({
title: '下载中...',
mask: true
})
// 模拟下载过程
setTimeout(() => {
uni.hideLoading()
isLoading.value = false
// 根据文件类型打开文件
if (fileName.endsWith('.PDF')) {
uni.showToast({
title: 'PDF文件已保存到下载目录',
icon: 'success',
duration: 2000
})
// 在真实项目中,这里应该调用下载API
// uni.downloadFile({
// url: 'https://example.com/files/filename.PDF',
// success: (res) => {
// if (res.statusCode === 200) {
// uni.saveFile({
// tempFilePath: res.tempFilePath,
// success: (saveRes) => {
// uni.showToast({ title: '下载成功', icon: 'success' })
// }
// })
// }
// }
// })
} else if (fileName.endsWith('.DOC')) {
uni.showToast({
title: 'DOC文件已保存到下载目录',
icon: 'success',
duration: 2000
})
}
}, 1500)
}
}
})
}
</script>
<style lang="scss" scoped>
.container {
min-height: 100vh;
background-color: #ffffff;
padding: 20px;
// font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
/* 内容滚动区域 */
.content-scroll {
padding-top: 15px;
box-sizing: border-box;
background-color: #ffffff;
}
/* 文章头部 */
.article-header {
// padding: 24px 20px 20px;
// border-bottom: 1px solid #f0f0f0;
.article-title {
font-size: 32rpx;
font-weight: bold;
color: #333333;
line-height: 1.4;
margin-bottom: 16px;
text-align: center;
}
.article-meta {
display: flex;
align-items: center;
justify-content: center;
color: #666666;
font-size: 14px;
.meta-item {
display: flex;
align-items: center;
.meta-icon {
font-size: 14px;
margin-right: 4px;
}
.meta-text {
font-size: 14px;
}
}
.meta-divider {
margin: 0 12px;
color: #cccccc;
}
}
}
/* 元信息 */
.notice-meta {
display: flex;
align-items: center;
justify-content: center;
// margin-bottom: 12px;
color: #666;
font-size: 16px;
margin: 0 30rpx;
.notice-author {
display: flex;
align-items: center;
.author-name {
color: #666;
}
.separator {
margin: 0 6px;
color: #ccc;
}
.publish-time {
color: #999;
}
}
.notice-time {
display: flex;
align-items: center;
.date-text {
margin-left: 4px;
color: #999;
}
}
}
/* 文章内容 */
.article-content {
padding: 20px;
color: #333333;
line-height: 1.8;
font-size: 16px;
.articleimg{
width: 100%;
height: 249rpx;
}
.paragraph {
margin-bottom: 20px;
text-align: justify;
text-indent: 2em;
}
.bullet-list {
margin: 20px 0 20px 2em;
.bullet-item {
display: flex;
margin-bottom: 12px;
.bullet-dot {
color: #666666;
margin-right: 8px;
flex-shrink: 0;
}
.bullet-text {
flex: 1;
color: #333333;
}
}
}
}
/* 附件区域 */
.attachments-section {
padding: 20px;
background-color: #f9f9f9;
border-top: 1px solid #f0f0f0;
border-bottom: 1px solid #f0f0f0;
margin-top: 20px;
.section-title {
display: flex;
align-items: center;
margin-bottom: 20px;
.title-text {
font-size: 18px;
font-weight: 600;
color: #333333;
white-space: nowrap;
margin-right: 12px;
}
.title-line {
flex: 1;
height: 1px;
background-color: #e0e0e0;
}
}
.attachments-list {
.attachment-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
background-color: #ffffff;
border-radius: 8px;
margin-bottom: 12px;
border: 1px solid #e8e8e8;
&:active {
background-color: #f5f5f5;
}
&:last-child {
margin-bottom: 0;
}
.attachment-left {
display: flex;
align-items: center;
flex: 1;
.file-icon {
font-size: 24px;
margin-right: 12px;
&.doc-icon {
color: #2b579a; /* Word文档蓝色 */
}
&.pdf-icon {
color: #e74c3c; /* PDF红色 */
}
}
.file-info {
.file-name {
font-size: 16px;
color: #333333;
margin-bottom: 4px;
font-weight: 500;
}
.file-size {
font-size: 13px;
color: #666666;
}
}
}
.download-btn {
width: 40px;
height: 40px;
border-radius: 20px;
background-color: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
.download-icon {
font-size: 18px;
color: #333333;
}
}
}
}
}
/* 页面底部 */
.page-footer {
padding: 30px 20px 40px;
display: flex;
flex-direction: column;
align-items: center;
.footer-text {
font-size: 16px;
color: #999999;
margin-bottom: 20px;
}
.footer-info {
display: flex;
align-items: center;
font-size: 13px;
color: #999999;
.info-item {
padding: 0 8px;
}
.info-divider {
color: #dddddd;
}
}
}
/* 响应式调整 */
@media (min-width: 768px) {
.container {
max-width: 768px;
margin: 0 auto;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
}
.article-header,
.article-content,
.attachments-section,
.page-footer {
padding-left: 40px;
padding-right: 40px;
}
}
/* 暗色模式适配 */
@media (prefers-color-scheme: dark) {
.container {
background-color: #1a1a1a;
}
.custom-navbar {
background-color: #1a1a1a;
border-bottom-color: #333333;
.nav-back,
.nav-title {
color: #ffffff;
}
.nav-actions .action-icon {
color: #ffffff;
}
}
.content-scroll {
background-color: #1a1a1a;
}
.article-header {
border-bottom-color: #333333;
.article-title {
color: #ffffff;
}
.article-meta {
color: #999999;
}
}
.article-content {
color: #e0e0e0;
.bullet-item .bullet-text {
color: #e0e0e0;
}
}
.attachments-section {
background-color: #2a2a2a;
border-top-color: #333333;
border-bottom-color: #333333;
.section-title .title-text {
color: #ffffff;
}
.attachment-item {
background-color: #2a2a2a;
border-color: #333333;
&:active {
background-color: #333333;
}
.file-info .file-name {
color: #ffffff;
}
.file-info .file-size {
color: #999999;
}
}
}
.page-footer {
.footer-text,
.footer-info {
color: #666666;
}
}
}
</style>

View File

@@ -0,0 +1,529 @@
<template>
<view class="container">
<!-- 搜索区域 -->
<view class="search-container">
<view class="search-input-wrapper">
<uni-icons class="search-icon" type="search" size="20" color="#999"></uni-icons>
<input
class="search-input"
placeholder="请输入关键字进行搜索"
placeholder-class="placeholder"
v-model="searchText"
@input="handleSearchInput"
/>
<view v-if="searchText" class="clear-icon" @tap="clearSearch">
<uni-icons type="clear" size="18" color="#999"></uni-icons>
</view>
</view>
<!-- <button class="search-btn" @tap="handleSearch">搜索</button> -->
</view>
<!-- 小区列表 -->
<scroll-view
class="community-list"
scroll-y
:scroll-into-view="scrollIntoViewId"
@scrolltolower="loadMoreData"
:lower-threshold="100"
>
<view
v-for="item in displayedCommunities"
:key="item.id"
class="community-item"
:id="'item-' + item.id"
@tap="selectCommunity(item)"
>
<view class="community-item-left">
<image src="/static/imgs/community.png" style="width: 48rpx;height: 48rpx;"></image>
<!-- <uni-icons class="house-icon" type="home" size="24" color="#666"></uni-icons> -->
<view class="community-info">
<text class="community-name">{{ item.name }}</text>
<!-- <text class="community-address">{{ item.address }}</text> -->
<!-- <text class="community-id">ID: {{ item.id }}</text> -->
</view>
</view>
<view v-if="selectedItem && selectedItem.id === item.id" class="selected-icon">
<!-- <uni-icons type="checkbox-filled" size="24" color="#007AFF"></uni-icons> -->
<image src="/static/imgs/check.png" style="width: 48rpx;height: 48rpx;"></image>
</view>
</view>
<!-- 加载更多 -->
<view v-if="hasMoreData" class="load-more-container">
<view v-if="!isLoading" class="load-more-btn" @tap="loadMoreData">
<text>加载更多</text>
<uni-icons type="arrow-down" size="16" color="#007AFF"></uni-icons>
</view>
<view v-else class="loading-container">
<uni-icons type="spinner-cycle" size="20" color="#007AFF" class="loading-icon"></uni-icons>
<text>正在加载...</text>
</view>
<!-- <text class="page-info">第 {{ currentPage }} 页 / 共 {{ totalPages }} 页</text> -->
</view>
<!-- 没有更多数据 -->
<view v-if="!hasMoreData && displayedCommunities.length > 0" class="no-more-data">
<text>没有更多数据了</text>
</view>
<!-- 空状态 -->
<view v-if="displayedCommunities.length === 0 && !isSearching" class="empty-state">
<uni-icons type="info" size="60" color="#ccc"></uni-icons>
<text class="empty-text">暂无小区数据</text>
</view>
<!-- 搜索无结果 -->
<view v-if="displayedCommunities.length === 0 && isSearching" class="empty-state">
<uni-icons type="search" size="60" color="#ccc"></uni-icons>
<text class="empty-text">未找到相关小区</text>
<text class="empty-tip">请尝试其他关键词</text>
</view>
</scroll-view>
<!-- 确认按钮 -->
<view class="confirm-area">
<button
class="confirm-btn"
:disabled="!selectedItem"
:class="{ 'disabled-btn': !selectedItem }"
@tap="handleConfirm"
>
确认选择
</button>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
// 响应式数据
const searchText = ref('')
const selectedItem = ref(null)
const scrollIntoViewId = ref('')
// 分页相关
const currentPage = ref(1)
const pageSize = 5
const totalCommunities = ref([])
const displayedCommunities = ref([])
const hasMoreData = ref(true)
const isLoading = ref(false)
// 搜索状态
const isSearching = ref(false)
let searchTimeout = null
// 所有小区数据
const allCommunities = ref([
{ id: 1, name: '北境碧桂园小区', address: '北境市光明区碧桂路1号' },
{ id: 2, name: '南湖花园小区', address: '南湖市滨湖区湖滨路28号' },
{ id: 3, name: '东城国际社区', address: '东城市新城区人民路156号' },
{ id: 4, name: '西湖雅苑', address: '西湖市风景区梅灵路88号' },
{ id: 5, name: '北境锦绣家园', address: '北境市朝阳区朝阳路56号' },
{ id: 6, name: '阳光100小区', address: '阳光市高新区创业路100号' },
{ id: 7, name: '绿城玫瑰园', address: '绿城市西湖区学院路188号' },
{ id: 8, name: '金地自在城', address: '金城市江干区金沙大道200号' },
{ id: 9, name: '万科城市花园', address: '万城市余杭区文一西路299号' },
{ id: 10, name: '保利中央公园', address: '保利市拱墅区莫干山路500号' },
{ id: 11, name: '龙湖天街小区', address: '龙湖市滨江区江南大道100号' },
])
// 计算属性
const totalPages = computed(() => {
return Math.ceil(totalCommunities.value.length / pageSize)
})
// 方法
// 加载分页数据
const loadPageData = (pageNum) => {
isLoading.value = true
// 模拟网络请求延迟
setTimeout(() => {
const startIndex = (pageNum - 1) * pageSize
const endIndex = startIndex + pageSize
const pageData = totalCommunities.value.slice(startIndex, endIndex)
if (pageNum === 1) {
displayedCommunities.value = pageData
} else {
displayedCommunities.value = [...displayedCommunities.value, ...pageData]
}
currentPage.value = pageNum
hasMoreData.value = displayedCommunities.value.length < totalCommunities.value.length
isLoading.value = false
// 滚动到新加载的内容
if (pageNum > 1 && pageData.length > 0) {
setTimeout(() => {
scrollIntoViewId.value = 'item-' + pageData[0].id
}, 100)
}
}, 500)
}
// 加载更多数据
const loadMoreData = () => {
if (isLoading.value || !hasMoreData.value) return
loadPageData(currentPage.value + 1)
}
// 处理搜索输入
const handleSearchInput = () => {
// 防抖处理
if (searchTimeout) {
clearTimeout(searchTimeout)
}
searchTimeout = setTimeout(() => {
handleSearch()
}, 300)
}
// 处理搜索
const handleSearch = () => {
const keyword = searchText.value.trim().toLowerCase()
if (!keyword) {
isSearching.value = false
totalCommunities.value = [...allCommunities.value]
loadPageData(1)
return
}
isSearching.value = true
totalCommunities.value = allCommunities.value.filter(community =>
community.name.toLowerCase().includes(keyword) ||
community.address.toLowerCase().includes(keyword)
)
loadPageData(1)
}
// 清空搜索
const clearSearch = () => {
searchText.value = ''
isSearching.value = false
totalCommunities.value = [...allCommunities.value]
loadPageData(1)
}
// 选择小区
const selectCommunity = (item) => {
selectedItem.value = item
}
// 确认选择
const handleConfirm = () => {
if (!selectedItem.value) {
uni.showToast({
title: '请先选择小区',
icon: 'none',
duration: 2000
})
return
}
uni.showModal({
title: '确认选择',
content: `您已选择: ${selectedItem.value.name}\n地址: ${selectedItem.value.address}`,
success: (res) => {
if (res.confirm) {
uni.showToast({
title: '选择成功',
icon: 'success',
duration: 1500
})
// 在实际应用中,这里可以触发回调或页面跳转
// uni.navigateBack() 或 emit('confirm', selectedItem.value)
uni.$emit('confirm', selectedItem.value)
// 模拟2秒后返回上一页
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
}
})
}
// 监听搜索文本变化
watch(searchText, (newVal) => {
if (!newVal.trim()) {
clearSearch()
}
})
// 初始化
onMounted(() => {
// 初始化数据
totalCommunities.value = [...allCommunities.value]
loadPageData(1)
// 默认选中第一项
if (displayedCommunities.value.length > 0) {
selectedItem.value = displayedCommunities.value[0]
}
})
</script>
<style scoped>
/* 页面容器 */
.container {
min-height: 100vh;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
padding: 0rpx 40rpx;
}
/* 搜索区域 */
.search-container {
padding: 20rpx 30rpx;
background-color: #ffffff;
display: flex;
align-items: center;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
z-index: 10;
position: sticky;
top: 0;
}
.search-input-wrapper {
flex: 1;
background-color: #f8f8f8;
border-radius: 10rpx;
padding: 0 20rpx;
display: flex;
align-items: center;
height: 80rpx;
margin-right: 20rpx;
}
.search-icon {
margin-right: 15rpx;
}
.search-input {
flex: 1;
height: 100%;
font-size: 28rpx;
color: #333;
}
.placeholder {
color: #999;
font-size: 28rpx;
}
.clear-icon {
padding: 10rpx;
display: flex;
align-items: center;
justify-content: center;
}
.search-btn {
background-color: #007AFF;
color: #fff;
font-size: 28rpx;
height: 80rpx;
line-height: 80rpx;
border-radius: 10rpx;
padding: 0 30rpx;
margin: 0;
white-space: nowrap;
}
/* 小区列表 */
.community-list {
flex: 1;
/* padding: 0 30rpx; */
margin-top: 20rpx;
margin-bottom: 180rpx;
}
.community-item {
background-color: #fff;
border-radius: 12rpx;
padding: 30rpx;
margin-bottom: 20rpx;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
transition: all 0.2s;
}
.community-item:active {
background-color: #f9f9f9;
transform: translateY(2rpx);
}
.community-item-left {
display: flex;
align-items: center;
flex: 1;
}
.house-icon {
margin-right: 20rpx;
}
.community-info {
display: flex;
flex-direction: column;
flex: 1;
}
.community-name {
font-size: 32rpx;
color: #333;
font-weight: 500;
margin-bottom: 8rpx;
}
.community-address {
font-size: 24rpx;
color: #999;
margin-bottom: 5rpx;
}
.community-id {
font-size: 22rpx;
color: #ccc;
}
.selected-icon {
margin-left: 20rpx;
}
/* 加载更多容器 */
.load-more-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 40rpx 0;
color: #999;
}
.load-more-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx 40rpx;
background-color: #f8f8f8;
border-radius: 50rpx;
color: #007AFF;
font-size: 28rpx;
margin-bottom: 20rpx;
}
.load-more-btn text {
margin-right: 10rpx;
}
.loading-container {
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx 40rpx;
background-color: #f8f8f8;
border-radius: 50rpx;
color: #007AFF;
font-size: 28rpx;
margin-bottom: 20rpx;
}
.loading-icon {
margin-right: 10rpx;
animation: rotate 1s linear infinite;
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.page-info {
font-size: 24rpx;
color: #999;
}
/* 没有更多数据 */
.no-more-data {
text-align: center;
padding: 40rpx 0;
color: #999;
font-size: 28rpx;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.empty-text {
font-size: 32rpx;
color: #999;
margin-top: 20rpx;
margin-bottom: 10rpx;
}
.empty-tip {
font-size: 24rpx;
color: #ccc;
}
/* 确认区域 */
.confirm-area {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 20rpx 30rpx 40rpx;
background-color: #fff;
box-shadow: 0 -2rpx 20rpx rgba(0, 0, 0, 0.05);
}
.confirm-btn {
background-color: #4D5DFA;
color: #fff;
height: 100rpx;
line-height: 100rpx;
border-radius: 8rpx;
font-size: 34rpx;
width: 100%;
margin: 0;
}
.disabled-btn {
background-color: #ccc;
color: #fff;
}
.selected-info {
text-align: center;
font-size: 24rpx;
color: #666;
margin-top: 20rpx;
padding: 10rpx;
background-color: #f8f8f8;
border-radius: 8rpx;
}
/* 适配不同屏幕 */
@media (min-width: 768px) {
.container {
max-width: 500px;
margin: 0 auto;
}
}
</style>

View File

@@ -0,0 +1,242 @@
<template>
<!-- 页面主容器 -->
<view class="door-open-page">
<view class="status_bar"></view>
<!-- 顶部标题栏 -->
<view class="top-bar">
<uni-nav-bar left-icon="left" title="远程开门" backgroundColor ="none" :border="false" @clickLeft="narBack" />
</view>
<view class="top-img">
<image src="/static/imgs/xuancheng-bg.png" mode="" style="width: 100%;height: 100%;"></image>
</view>
<!-- 提示文案 -->
<view class="tips-text">
远程开门
<text class="tips-desc">请选择要开的门并保持距离在10米以内,单次仅开一门</text>
</view>
<!-- 开门成功提示(默认隐藏) -->
<view v-if="showSuccess" class="success-toast">开门成功</view>
<!-- 开门授权区域 -->
<view class="auth-container">
<view class="auth-title">开门授权</view>
<!-- 网格布局:2行2列 -->
<view class="grid-container">
<!-- 每个门选项 -->
<view
v-for="(item, index) in doorList"
:key="index"
:class="['door-item', { active: selectedDoor === index }]"
@click="selectDoor(index)"
>
<!-- 图标占位(可替换为真实图标/图片) -->
<view class="door-icon">
<image class="icon-text" :src="selectedDoor === index ? item.iconActive:item.iconText"></image>
</view>
<!-- 门名称 -->
<text class="door-name">{{ item.name }}</text>
</view>
</view>
</view>
<!-- 确认开门按钮 -->
<button class="confirm-btn" @click="openDoor" :disabled="selectedDoor === -1">确认开门</button>
</view>
</template>
<script setup>
import { ref } from 'vue'
// 响应式数据:选中的门索引、开门成功提示
const selectedDoor = ref(-1) // -1表示未选中
const showSuccess = ref(false)
// 门列表数据()
const doorList = ref([
{ iconText: '/static/imgs/westDoor.png',iconActive:"/static/imgs/westDoorWhite.png", name: '西入门闸机' },
{ iconText: '/static/imgs/eastDoor.png',iconActive:"/static/imgs/eastDoorWhite.png", name: '东入门闸机' },
{ iconText: '/static/imgs/unitDoor.png',iconActive:"/static/imgs/unitDoorWhite.png", name: '1号楼1单元门' },
{ iconText: '/static/imgs/unitDoor.png',iconActive:"/static/imgs/unitDoorWhite.png", name: '1号楼2单元门' }
])
// 选择门的方法
const selectDoor = (index) => {
selectedDoor.value = index
showSuccess.value = false // 选择新门时隐藏成功提示
}
// 返回
const narBack = () =>{
uni.switchTab({
url:"/pages/index/index"
})
}
// 确认开门方法
const openDoor = () => {
// 校验:未选择门时提示
// if (selectedDoor.value === -1) {
// uni.showToast({
// title: '请先选择要开的门',
// icon: 'none'
// })
// return
// }
// 模拟开门接口请求(实际项目替换为真实接口)
setTimeout(() => {
showSuccess.value = true
// 可选:添加成功提示后自动隐藏
// setTimeout(() => showSuccess.value = false, 2000)
}, 500)
}
</script>
<style scoped>
/* 页面全局样式 */
.door-open-page {
background: linear-gradient(180deg, #E2F0FF, #F6FAFF);
min-height: 100vh;
padding: 30rpx 40rpx;
box-sizing: border-box;
}
.status_bar {
height: 32px;
width: 100%;
}
/* 顶部标题 */
.top-bar {
text-align: center;
margin-bottom: 20rpx;
}
.title-text {
font-size: 48rpx;
color: #ffffff;
font-weight: bold;
}
.top-img{
width: 460rpx;
height: 290rpx;
float: right;
}
/* 提示文案 */
.tips-text {
color: #000;
margin-bottom: 30rpx;
font-size: 68rpx;
line-height: 1.5;
font-weight: 700;
font-style: italic;
clear: both;
position: relative;
top: -86px;
left: 0;
}
.tips-desc {
display: block;
font-size: 24rpx;
opacity: 0.9;
color: #666666;
}
/* 开门成功提示 */
.success-toast {
background: rgba(0, 0, 0, 0.7);
color: #ffffff;
padding: 10rpx 20rpx;
border-radius: 30rpx;
font-size: 24rpx;
position: absolute;
top: 200rpx;
left: 50%;
transform: translateX(-50%);
z-index: 99;
}
/* 开门授权容器 */
.auth-container {
background: #ffffff;
border-radius: 20rpx;
padding: 30rpx 20rpx;
margin-bottom: 60rpx;
position: relative;
top: -86px;
left: 0;
}
.auth-title {
text-align: center;
font-size: 32rpx;
color: #333333;
padding-bottom: 20rpx;
border-bottom: 1px solid #e5e5e5;
margin-bottom: 30rpx;
}
/* 网格布局 */
.grid-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30rpx;
}
/* 门选项样式 */
.door-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 30rpx 20rpx;
border-radius: 10rpx;
border: 2px solid #2F77FD;
background-color: #F6F9FE;
transition: all 0.3s;
}
/* 选中状态 */
.door-item.active {
/* border-color: #2c6bc7; */
background-color: #4D5DFA;
color: #ffffff;
}
.door-item.active .door-name {
color: #ffffff !important;
}
/* 门图标 */
.door-icon {
width: 80rpx;
height: 80rpx;
/* background-color: #f5f5f5; */
border-radius: 10rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 15rpx;
}
.icon-text {
/* font-size: 40rpx; */
width: 100%;
height: 100%;
}
/* 门名称 */
.door-name {
font-size: 26rpx;
color: #333333;
}
/* 确认按钮 */
.confirm-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background-color: #4D5DFA;
color: #fff;
font-size: 32rpx;
border-radius: 20rpx;
border: none;
}
.confirm-btn::after {
border: none; /* 去除uni-app默认按钮边框 */
}
</style>