This commit is contained in:
2026-09-29 15:17:13 +08:00
commit 69e6ed8c59
113 changed files with 36564 additions and 0 deletions
+122
View File
@@ -0,0 +1,122 @@
<template>
<view class="app-navbar" :style="{'padding-top': `${getSafeTop()}px`, 'padding-bottom': '40px'}">
<view class="navbar-left" :style="{top: `${getSafeTop()}px`}" v-if="showBack" @click="back">
<u-icon name="arrow-left" color="white" size="40rpx" v-if="currentPages.length > 1"/>
<u-icon name="home" color="white" size="40rpx" v-else/>
</view>
<text class="navbar-title" :style="{ fontSize: titleSize }">{{ title }}</text>
</view>
</template>
<script>
export default {
props: {
title: {
type: String,
default: ''
},
titleSize: {
type: String,
default: '34rpx'
},
showBack: {
type: Boolean,
default: false
}
},
data() {
return {
currentPages: []
}
},
methods: {
getSafeTop() {
const systemInfo = uni.getSystemInfoSync()
return systemInfo.statusBarHeight
},
back(){
if (this.currentPages.length === 1) uni.switchTab({url: '/pages/index/index'})
else uni.navigateBack()
}
},
mounted() {
this.currentPages = getCurrentPages()
}
}
</script>
<style lang="scss" scoped>
.app-navbar {
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: 0 10rpx;
min-height: 56rpx;
background: url('https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260126/202ad055d8706f14cd94fae4a8bbc14c.png') no-repeat center/cover;
}
.navbar-left {
width: 50rpx;
height: 50rpx;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
left: 20rpx;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 999rpx;
}
.navbar-right {
display: flex;
align-items: center;
}
.navbar-title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 34rpx;
font-weight: 700;
color: #111827;
letter-spacing: 2rpx;
}
.navbar-capsule {
display: flex;
align-items: center;
gap: 12rpx;
height: 52rpx;
padding: 0 16rpx;
background: #ffffff;
border-radius: 999rpx;
box-shadow: 0 8rpx 16rpx rgba(0, 0, 0, 0.08);
}
.capsule-dots {
display: flex;
gap: 6rpx;
}
.capsule-dot {
width: 8rpx;
height: 8rpx;
border-radius: 50%;
background: #111827;
}
.capsule-divider {
width: 1rpx;
height: 24rpx;
background: #d1d5db;
}
.capsule-ring {
width: 26rpx;
height: 26rpx;
border-radius: 50%;
border: 3rpx solid #111827;
}
</style>
+250
View File
@@ -0,0 +1,250 @@
<template>
<u-popup :show="show" mode="bottom" round="24rpx" close-on-click-overlay @close="handleClose">
<view class="area-picker">
<view class="area-picker-head">
<text class="area-picker-title">选择省市</text>
<view class="area-picker-actions">
<view class="area-picker-btn" @tap="handleClose">取消</view>
<view class="area-picker-btn primary" @tap="confirmSelection">确定</view>
</view>
</view>
<view class="area-picker-body">
<view v-if="loading" class="area-loading">加载中...</view>
<view v-else class="area-columns">
<view class="area-column">
<scroll-view scroll-y class="area-scroll">
<view
v-for="(item, index) in provinces"
:key="index"
:class="['area-item', index === provinceIndex ? 'active' : '']"
@tap="selectProvince(index)"
>
{{ item.name }}
</view>
</scroll-view>
</view>
<view class="area-column">
<scroll-view scroll-y class="area-scroll">
<view v-if="cities.length === 0" class="area-empty">暂无城市</view>
<view
v-for="(item, index) in cities"
:key="index"
:class="['area-item', index === cityIndex ? 'active' : '']"
@tap="selectCity(index)"
>
{{ item.name }}
</view>
</scroll-view>
</view>
</view>
</view>
</view>
</u-popup>
</template>
<script>
import {areaListReq, areaListByFidReq} from "@/api/common";
export default {
name: 'AreaPicker',
props: {
show: {
type: Boolean,
default: false
},
defaultProvince: {
type: String,
default: ''
},
defaultCity: {
type: String,
default: ''
}
},
data() {
return {
loading: false,
provinces: [],
cities: [],
provinceIndex: 0,
cityIndex: 0
}
},
watch: {
show(val) {
if (val) {
this.initAreas()
}
}
},
methods: {
handleClose() {
this.$emit('close')
},
normalizeAreaItem(item) {
return {
id: item.areaId || item.code || item.value || item.id || null,
name: item.name || item.title || item.areaName || item.label || ''
}
},
async fetchProvinces() {
const {data} = await areaListReq({level: 1})
const list = Array.isArray(data) ? data : (data && data.list ? data.list : [])
return list.map(this.normalizeAreaItem)
},
async fetchCities(fid) {
const {data} = await areaListByFidReq({fid})
const list = Array.isArray(data) ? data : (data && data.list ? data.list : [])
return list.map(this.normalizeAreaItem)
},
findIndexByName(list, name) {
if (!name) return -1
return list.findIndex(item => item.name === name)
},
async initAreas() {
this.loading = true
try {
if (!this.provinces.length) {
this.provinces = await this.fetchProvinces()
}
let provinceIndex = this.findIndexByName(this.provinces, this.defaultProvince)
if (provinceIndex < 0) provinceIndex = 0
this.provinceIndex = provinceIndex
const province = this.provinces[provinceIndex]
this.cities = province ? await this.fetchCities(province.id) : []
let cityIndex = this.findIndexByName(this.cities, this.defaultCity)
if (cityIndex < 0) cityIndex = 0
this.cityIndex = cityIndex
} catch (err) {
uni.showToast({
title: '地区加载失败',
icon: 'none'
})
} finally {
this.loading = false
}
},
async selectProvince(index) {
if (index === this.provinceIndex) return
this.provinceIndex = index
this.cityIndex = 0
const province = this.provinces[index]
if (!province) {
this.cities = []
return
}
this.loading = true
try {
this.cities = await this.fetchCities(province.id)
this.cityIndex = 0
} finally {
this.loading = false
}
},
selectCity(index) {
this.cityIndex = index
},
confirmSelection() {
const province = this.provinces[this.provinceIndex]
const city = this.cities[this.cityIndex]
if (!province || !city) {
uni.showToast({
title: '请选择省市',
icon: 'none'
})
return
}
this.$emit('confirm', {
province,
city,
provinceId: province.id,
cityId: city.id,
provinceName: province.name,
cityName: city.name
})
}
}
}
</script>
<style lang="scss" scoped>
.area-picker {
background: #ffffff;
padding: 20rpx 20rpx 30rpx;
border-radius: 24rpx 24rpx 0 0;
}
.area-picker-head {
display: flex;
align-items: center;
justify-content: space-between;
}
.area-picker-title {
font-size: 28rpx;
font-weight: 700;
color: #1f2937;
}
.area-picker-actions {
display: flex;
align-items: center;
gap: 16rpx;
}
.area-picker-btn {
font-size: 24rpx;
color: #6b7280;
}
.area-picker-btn.primary {
color: #2fc6a7;
font-weight: 700;
}
.area-picker-body {
margin-top: 16rpx;
}
.area-loading {
padding: 24rpx 0;
text-align: center;
font-size: 24rpx;
color: #9ca3af;
}
.area-columns {
display: flex;
gap: 16rpx;
}
.area-column {
flex: 1;
border-radius: 16rpx;
background: #f8fafc;
overflow: hidden;
}
.area-scroll {
height: 60vh;
}
.area-item {
padding: 16rpx 18rpx;
font-size: 26rpx;
color: #374151;
}
.area-item.active {
color: #2fc6a7;
font-weight: 700;
background: #e6fffa;
}
.area-empty {
padding: 18rpx;
font-size: 24rpx;
color: #9ca3af;
text-align: center;
}
</style>
+41
View File
@@ -0,0 +1,41 @@
<template>
<u-sticky zIndex="99999">
<u-navbar fixed :auto-back="false" :bgColor="bgColor" :status-bar-bg-color="bgColor" placeholder :title="title" safeAreaInsetTop
:title-style="{color: 'white'}" auto-back left-icon-color="white" @leftClick="leftClick">
<template slot="left">
<u-icon :name="currentPages.length > 1 ? 'arrow-left' : 'home'" size="20" color="white" v-if="showBack"/>
</template>
</u-navbar>
</u-sticky>
</template>
<script>
import {LINEAR} from "@/config/color";
export default {
name: "HeaderItem",
props: {
title: String,
showBack: {
type: Boolean,
default: true
}
},
data() {
return {
bgColor: LINEAR,
currentPages: []
}
},
methods: {
leftClick() {
console.log(this.currentPages)
if (this.currentPages.length === 1) uni.switchTab({url: '/pages/index/index'})
// else uni.navigateBack()
}
},
mounted() {
this.currentPages = getCurrentPages()
}
}
</script>
+222
View File
@@ -0,0 +1,222 @@
<template>
<u-popup :show="show" :close-on-click-overlay="false" @close="refuse" round="10" :safe-area-inset-bottom="false">
<view class="p-50 text-black">
<template v-if="mode === 'wechat'">
<view class="flex flex-col justify-start items-start">
<!-- <u-icon name="/static/logo.png" width="100" height="52"/>-->
<text class="text-30 font-bold mt-20">为了提供更优质的服务,需要获取</text>
</view>
<view class="my-25">
<u-text text="您的身份信息" size="16" bold margin="60rpx 0"/>
</view>
<view class="mt-20 text-25">
<checkbox-group @change="changeCheck">
<view class="flex justify-start items-center">
<checkbox value="check" :checked="check"/>
<view>
<text>我已阅读并同意</text>
<text style="color: #00004F" @click="showAgree('USER_PROTOCOL')">
《用户协议》
</text>
<text style="color: #00004F" @click="showAgree('SECRET_PROTOCOL')">
《隐私协议》
</text>
</view>
</view>
</checkbox-group>
</view>
<view class="mt-25 flex justify-between">
<view class="w-40p">
<u-button type="info" @click="refuse" :custom-style="{backgroundColor: '#F2F2F2'}">拒绝</u-button>
</view>
<view class="w-40p">
<u-button @getphonenumber="auth" open-type="getPhoneNumber"
:custom-style="agreeStyle"
:disabled="!check || disabled">允许
</u-button>
</view>
</view>
</template>
<template v-else>
<u-form label-width="140rpx">
<u-form-item borderBottom label="手机号">
<u-input v-model="phone" placeholder="请输入手机号"/>
</u-form-item>
<u-form-item borderBottom label="密码">
<u-input v-model="pwd" type="password" placeholder="请输入密码"/>
</u-form-item>
</u-form>
<view class="mt-20 text-25">
<checkbox-group @change="changeCheck">
<view class="flex justify-start items-center">
<checkbox value="check" :checked="check"/>
<view>
<text>我已阅读并同意</text>
<text style="color: #00004F" @click="showAgree('USER_PROTOCOL')">
《用户协议》
</text>
<text style="color: #00004F" @click="showAgree('SECRET_PROTOCOL')">
《隐私协议》
</text>
</view>
</view>
</checkbox-group>
</view>
<view class="mt-25 flex justify-between">
<view class="w-40p">
<u-button type="info" @click="refuse" :custom-style="{backgroundColor: '#F2F2F2'}">关闭</u-button>
</view>
<view class="w-40p">
<u-button @click="loginByPhone" :custom-style="agreeStyle"
:disabled="!check || disabled || !phone || !pwd">登录
</u-button>
</view>
</view>
</template>
<view class="flex mt-25 justify-center text-25" style="color: #00004F" @click="mode = 'phone'"
v-if="mode === 'wechat'">手机号登录
</view>
<view class="flex mt-25 justify-center text-25" style="color: #00004F" @click="mode = 'wechat'" v-else>一键授权登录
</view>
</view>
<u-popup mode="center" :show="showAgreePopup" closeable @close="showAgreePopup = false" round="10"
:custom-style="{height: '80%', width: '80%'}" closeOnClickOverlay>
<view class="p-30 overflow-y">
<u-parse :content="content"/>
</view>
</u-popup>
</u-popup>
</template>
<script>
import {loginByPhoneForTestReq, loginByPhoneReq, loginReq} from "@/api/user";
import {clearShareUserId, getShareUserId, setUserInfo} from "@/util/user";
import {articleInfoReq, sysSettingInfoReq} from "@/api/common";
export default {
name: "Login",
props: {
isIndex: {
type: Boolean,
default: false
},
isHide: {
type: Boolean,
default: false
}
},
data() {
return {
show: false,
check: false,
code: null,
phoneData: null,
agreeStyle: {
backgroundColor: this.check ? '#00C655' : '#F2F2F2',
border: this.check ? '#00C655' : '#F2F2F2',
color: this.check ? '#FFFFFF' : '#828282',
},
showAgreePopup: false,
content: '',
phone: '',
pwd: '',
mode: 'wechat',
disabled: false
}
},
methods: {
changeCheck({detail}) {
const {value} = detail
this.check = value.length > 0
this.agreeStyle = {
backgroundColor: this.check ? '#00C655' : '#F2F2F2',
border: this.check ? '#00C655' : '#F2F2F2',
color: this.check ? '#FFFFFF' : '#828282',
}
},
open() {
this.disabled = false
this.show = true
},
hide() {
this.show = false
},
async loginByPhone() {
this.disabled = true
const shareUserId = getShareUserId()
const {data} = await loginByPhoneForTestReq({
phone: this.phone,
phoneLoginCode: this.pwd,
...(shareUserId ? {shareUserId} : {})
})
console.log(data)
this.setInfo(data)
clearShareUserId()
this.done()
this.disabled = false
},
wxLogin() {
return new Promise(resolve => {
uni.login({
success: (loginRes) => {
this.code = loginRes.code
resolve()
}
})
})
},
auth(phoneE) {
//获取手机号
this.phoneData = phoneE.detail
this.doLogin()
},
async doLogin() {
await this.wxLogin()
this.disabled = true
const phoneCode = this.phoneData.code
const parentId = getShareUserId()
const {data} = await loginByPhoneReq({
phoneCode,
code: this.code,
...(parentId ? {parentId} : {})
})
this.setInfo(data)
clearShareUserId()
if (data.user.authStatus !== 2) {
this.show = false
return uni.navigateTo({url: '/pages/certification/index'})
} else {
if (!data.user.realName) {
this.show = false
return uni.navigateTo({url: '/pages/mine/profile'})
}
}
this.done()
this.disabled = false
},
setInfo(res) {
setUserInfo({
username: res.user.nickname,
header: res.user.avatar,
phone: res.phone || res.user.phone,
token: res.access_token,
uid: res.user.userId,
});
},
done() {
this.show = false
this.$emit('done')
},
refuse() {
this.show = false
if (!this.isHide) uni.switchTab({url: '/pages/index/index'})
},
async showAgree(enumKey) {
const id = enumKey === 'USER_PROTOCOL' ? 9854 : 9855
const {data} = await articleInfoReq(id)
this.content = data.content
this.showAgreePopup = true
}
},
}
</script>
+18
View File
@@ -0,0 +1,18 @@
<template>
<view class="mb-25 mt-25">
<u-icon name="/static/line.png" :label="title" label-pos="right" bold label-color="#4F4F4F" label-size="30rpx"/>
</view>
</template>
<script>
export default {
name: "TitleItem",
props: {
title: String
},
data() {
return {}
},
methods: {}
}
</script>
+120
View File
@@ -0,0 +1,120 @@
<template>
<view>
<m-tabbar fixed fill :current="current" safeBottom :tabbar="tabbar" native openType="switchTab" @click="tabClick">
<!-- <template v-slot:tabbar_index_2>-->
<!-- <view class="custom_style">-->
<!-- <view class="custom_style_icon">-->
<!-- <u-image v-if="!isAuditing" src="https://oss.wsdns.cn/20250627/b6ea18396b074f8db2f649c7db385419.png" width="160rpx"-->
<!-- height="160rpx"/>-->
<!-- <u-image v-else src="https://img.ggsxiangan.com/62c543af60204ff8868df41a31f79ec0.png" width="160rpx"-->
<!-- height="160rpx"/>-->
<!-- </view>-->
<!-- </view>-->
<!-- </template>-->
</m-tabbar>
</view>
</template>
<script>
import {dictDataInfoReq} from "@/api/common";
export default {
name: "customTabbar",
props: {
current: {
default: 0,
type: Number
}
},
data() {
return {
tabbar: {
color: "#333",
selectedColor: "#98C147",
borderStyle: "F0F0F0",
backgroundColor: "#FFFFFF",
list: [
{
"pagePath": "pages/index/index",
"iconPath": "static/tabbar/home.png",
"selectedIconPath": "static/tabbar/home-active.png",
"text": "首页"
},
{
"pagePath": "pages/index/index",
"iconPath": "static/tabbar/service.png",
"selectedIconPath": "static/tabbar/service-active.png",
"text": "服务"
},
{
"pagePath": "pages/community/index",
"iconPath": "static/tabbar/xiaoqu.png",
"selectedIconPath": "static/tabbar/xiaoqu-active.png",
"text": "小区"
},
{
"pagePath": "pages/index/index",
"iconPath": "static/tabbar/store.png",
"selectedIconPath": "static/tabbar/store-active.png",
"text": "商城"
},
{
"pagePath": "pages/user/index",
"iconPath": "static/tabbar/user.png",
"selectedIconPath": "static/tabbar/user-active.png",
"text": "我的"
}
]
},
isAuditing: true
}
},
methods: {
tabClick(val) {
console.log('val', val)
if (val === 0 && this.current === 0) this.$jump('/pages/index/index')
if (val === 3) {
uni.navigateToMiniProgram({
appId: 'wxad2376fd45fb7eca',
complete() {
uni.switchTab({url: '/pages/index/index'})
},
})
}
},
async checkAudit() {
const {data} = await dictDataInfoReq(3678)
this.isAuditing = data.dictDataCode === '1'
}
},
mounted() {
this.checkAudit()
},
}
</script>
<style lang="scss">
.custom_style {
color: #fff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 24rpx;
width: 100rpx;
height: 100rpx;
margin-top: -40rpx;
&_icon {
//background-color: #2ACF8D;
font-size: 80rpx;
width: 100rpx;
height: 100rpx;
border-radius: 100%;
display: flex;
justify-content: center;
align-items: center;
}
}
</style>