迁移版本

This commit is contained in:
李琦
2026-06-05 14:06:20 +08:00
commit 139ce6b2f8
564 changed files with 62049 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
node_modules
.project
unpackage/
.DS_Store
.hbuilderx
.idea

74
App.vue Normal file
View File

@@ -0,0 +1,74 @@
<script>
import { loginApi } from "@/api/page/auth";
import { getCache, setCache } from "@/utils/cache";
export default {
onLaunch: function () {
console.log("App Launch");
// 检查更新
if (uni.canIUse('getUpdateManager')) {
const updateManager = uni.getUpdateManager();
updateManager.onCheckForUpdate(function (res) {
// 请求完新版本信息的回调
if (res.hasUpdate) {
console.log("有新版本");
}
});
updateManager.onUpdateReady(function () {
uni.showModal({
title: "更新提示",
content: "新版本已经准备好,是否重启应用?",
success: function (res) {
if (res.confirm) {
// 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
updateManager.applyUpdate();
}
},
});
});
updateManager.onUpdateFailed(function () {
// 新版本下载失败
uni.showModal({
title: "更新提示",
content: "新版本下载失败,请检查网络后重试",
showCancel: false,
});
});
}
},
onShow: function () {
console.log("App Show");
const token = getCache("token");
if (!token || token === "" || token === null) {
uni.login({
provider: "weixin",
success: (loginRes) => {
if (loginRes.code) {
loginApi(
loginRes.code,
this.phoneCode,
"",
"微信用户" + Math.random().toString(36).substring(2)
).then((result) => {
setCache("token", result.token);
setCache("user_info", result.user_info);
});
} else {
console.error("登录失败!" + loginRes.errMsg);
}
},
});
}
},
onHide: function () {
console.log("App Hide");
},
};
</script>
<style lang="scss">
@import "uview-plus/index.scss";
</style>

114
api/interceptor.js Normal file
View File

@@ -0,0 +1,114 @@
// 定义一个全局请求配置(可被多个拦截器修改)
import {setCache} from "@/utils/cache";
export let globalConfig = {
baseURL: 'https://wx.api.borman.top/api/',
// baseURL: 'http://brm.wx.api.borman.top/api/',
// baseURL: 'https://wx.boerman.top/api/',
// baseURL: 'http://127.0.0.1:18004/api/', // 本地测试地址
// baseURL: 'http://127.0.0.1:18006/api/', // 本地测试地址
header: {
'Content-Type': 'application/json'
}
};
// 请求拦截器队列
const requestInterceptors = [];
// 添加请求拦截器
export function addRequestInterceptor(interceptor) {
requestInterceptors.push(interceptor);
}
// 执行请求拦截器
export async function runRequestInterceptors(config) {
let newConfig = { ...config };
for (const interceptor of requestInterceptors) {
newConfig = await interceptor(newConfig);
}
return newConfig;
}
// 响应拦截器队列
const responseInterceptors = [];
// 添加响应拦截器
export function addResponseInterceptor(interceptor) {
responseInterceptors.push(interceptor);
}
// 执行响应拦截器
export async function runResponseInterceptors(response) {
let newResponse = { ...response };
for (const interceptor of responseInterceptors) {
newResponse = await interceptor(newResponse);
}
return newResponse;
}
// 初始化默认拦截器(可选)
addRequestInterceptor(config => {
// 自动携带token示例
const token = uni.getStorageSync('token');
// 临时token测试用
// const token = 'JG7hKhTQoANh/hyL67BzIg==';
if (token) {
config.header.Authorization = `Bearer ${token}`;
}
return config;
});
addResponseInterceptor(response => {
// 统一处理错误状态码
if (response.statusCode !== 200) {
uni.showToast({ title: `请求错误: ${response.data.message}`, icon: 'none' });
return Promise.reject(response.data.message);
}
if (response.data.code === 500) {
uni.showToast({ title: response.data.message, icon: 'none' });
return Promise.reject(response.data.message);
}
if (response.data.code === 401) {
uni.showToast({ title: '登录已过期,请重新登录', icon: 'none' });
setCache('token', null);
setTimeout(() => {
// 储存当前访问的路由
// 获取当前页面栈
const pages = getCurrentPages();
if (pages.length === 0) {
console.error('未找到当前页面栈');
return;
}
// 获取当前页面实例
const currentPage = pages[pages.length - 1];
if (!currentPage) {
console.error('未找到当前页面实例');
return;
}
// 获取当前页面的路由地址
const currentRoute = currentPage.route;
if (!currentRoute) {
console.error('未找到当前页面的路由地址');
return;
}
// 获取传参
const options = currentPage.options;
// 构建包含参数的查询字符串
let queryString = '';
if (Object.keys(options).length > 0) {
queryString = '?' + Object.entries(options)
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
.join('&');
}
// 拼接完整的路由地址
const fullRoute = `/${currentRoute}${queryString}`;
// 设置缓存
setCache('currentPage', fullRoute);
uni.reLaunch({ url: '/pages/login/index' });
}, 1000)
}
return response.data.result; // 根据实际接口返回结构调整
});

11
api/page/auth.js Normal file
View File

@@ -0,0 +1,11 @@
import {get, post} from '../request';
const prefix = 'auth/'
export const loginApi = async (code, phoneCode, avatarUrl, nickName) => {
return await post(`${prefix}login`, {
code,
phone_code: phoneCode,
avatar: avatarUrl,
nick_name: nickName
})
}

30
api/page/cart.js Normal file
View File

@@ -0,0 +1,30 @@
import {get, post} from '../request';
const prefix = 'list/'
export const getCartListApi = async () => {
return await get(`${prefix}list`)
}
export const getCartItemApi = async (id) => {
return await get(`${prefix}detail`, {id})
}
export const createCartApi = async (params) => {
return await post(`${prefix}create`, params)
}
export const deleteCartApi = async (id) => {
return await post(`${prefix}delete`, {
ids: [id]
})
}
export const deleteCartItemApi = async (id) => {
return await post(`${prefix}delete-item`, {
ids: [id]
})
}
export const toCartApi = async (listId, productId) => {
return await post(`${prefix}to-cart`, {
list_id: listId,
product_id: productId
})
}

10
api/page/home.js Normal file
View File

@@ -0,0 +1,10 @@
import {get} from '../request';
const prefix = 'home/'
export const getCarouselApi = async () => {
return await get(`${prefix}carousel`)
}
export const getCategoryListApi = async () => {
return await get(`${prefix}category-list`)
}

15
api/page/image.js Normal file
View File

@@ -0,0 +1,15 @@
import {get, post} from '../request';
const prefix = 'w-oss/'
export const getImageApi = async (hash = '') => {
return await get(`${prefix}getImage`, {
hash
})
}
export const uploadImageWatermarkApi = async (imageUrl = '', watermark = 'BRM') => {
return await post(`${prefix}watermark`, {
image_url: imageUrl,
watermark
})
}

14
api/page/product.js Normal file
View File

@@ -0,0 +1,14 @@
import {get} from '../request';
const prefix = 'product/'
export const getProductListApi = async (param) => {
return await get(`${prefix}list`, param)
}
export const getCategoryListByPidApi = async (pid = 0) => {
return await get(`${prefix}category-list-by-pid`, {pid})
}
export const getProductDetailApi = async (id = 0, p_user_id = 0) => {
return await get(`${prefix}detail`, {id, p_user_id})
}

17
api/page/user.js Normal file
View File

@@ -0,0 +1,17 @@
import {get, post} from '../request';
const prefix = 'user/'
export const getUserInfoApi = async () => {
return await get(`${prefix}my-info`)
}
export const bandPhoneApi = async (phone) => {
return await post(`${prefix}band-phone`, {
phone: phone
})
}
export const updateNickNameApi = async (nick_name) => {
return await post(`${prefix}update-nick-name`, {
nick_name
})
}

70
api/request.js Normal file
View File

@@ -0,0 +1,70 @@
import {
runRequestInterceptors,
runResponseInterceptors,
globalConfig
} from './interceptor.js';
// import * as uni from "@/api/request"; // 原错误导入保留供参考
// 基础请求方法
export function request(options) {
return new Promise(async (resolve, reject) => {
try {
// 合并全局配置
const mergedConfig = {
...globalConfig,
...options,
header: { ...globalConfig.header, ...(options.header || {}) }
};
// 执行请求拦截器
let finalConfig;
try {
finalConfig = await runRequestInterceptors(mergedConfig);
} catch (err) {
return reject(err);
}
if (!finalConfig.url) {
return reject(new Error('URL is required'));
}
// 发起请求修复点移除多余的await
uni.request({
...finalConfig,
url: finalConfig.baseURL + finalConfig.url,
success: async (res) => {
try {
const handledRes = await runResponseInterceptors(res);
// console.log(handledRes, 'ssssssssssssssss'); // 保留调试日志
resolve(handledRes);
} catch (err) {
reject(err);
}
},
fail: (err) => reject(err)
});
} catch (error) {
reject(error);
}
});
}
// 封装GET方法保持async/await结构
export async function get(url, params = {}, options = {}) {
return await request({
url,
method: 'GET',
data: params,
...options
});
}
// 封装POST方法保持async/await结构
export async function post(url, data = {}, options = {}) {
return await request({
url,
method: 'POST',
data,
...options
});
}

3
common/api.js Normal file
View File

@@ -0,0 +1,3 @@
const { http } = uni.$u
// 获取菜单
export const fetchMenu = (params, config = {}) => http.post('/ebapi/public_api/index', params, config)

1
common/area.js Normal file

File diff suppressed because one or more lines are too long

1
common/city.js Normal file

File diff suppressed because one or more lines are too long

1087
common/classify.data.js Normal file

File diff suppressed because it is too large Load Diff

3
common/config.js Normal file
View File

@@ -0,0 +1,3 @@
export default {
baseUrl: 'https://uview-plus.lingyun.net'
}

55
common/demo.scss Normal file
View File

@@ -0,0 +1,55 @@
.u-block {
padding: 14px;
&__section {
margin-bottom:10px;
}
&__title {
margin-top:10px;
font-size: 15px;
color: $u-content-color;
margin-bottom:10px;
}
&__flex {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
}
}
// 使用了cell组件的icon图片样式
.u-cell-icon {
width: 36rpx;
height: 36rpx;
margin-right: 8rpx;
}
.u-page {
padding: 15px 15px 40px 15px;
&__item {
flex: 1;
// margin-bottom: 23px;
&__title {
font-size: 14px;
color: rgb(143, 156, 162);
// margin-bottom: 8px;
@include flex;
}
}
}
.u-demo-block {
flex: 1;
margin-bottom: 23px;
&__content {
@include flex(column);
}
&__title {
font-size: 14px;
color: rgb(143, 156, 162);
margin-bottom: 8px;
@include flex;
}
}

21
common/locales/en.js Normal file
View File

@@ -0,0 +1,21 @@
export default {
// 可以以页面为单位来写比如首页的内容写在index字段个人中心写在center共同部分写在common部分
components: {
desc: 'Numerous components cover the various requirements of the development process, and the components are rich in functions and compatible with multiple terminals. Let you integrate quickly, out of the box'
},
js: {
desc: 'Numerous intimate gadgets are a weapon that you can call upon during the development process, allowing you to dart in your hand and pierce the Yang with a hundred steps'
},
template: {
desc: 'Collection of many commonly used pages and layouts, reducing the repetitive work of developers, allowing you to focus on logic and get twice the result with half the effort'
},
nav: {
components: 'Components',
js: 'JS',
template: 'Template'
},
common: {
intro: 'UI framework for rapid development of multiple platforms',
title: 'uview-plus',
},
}

21
common/locales/zh.js Normal file
View File

@@ -0,0 +1,21 @@
export default {
// 可以以页面为单位来写比如首页的内容写在index字段个人中心写在center共同部分写在common部分
components: {
desc: '众多组件覆盖开发过程的各个需求,组件功能丰富,多端兼容。让你快速集成,开箱即用'
},
js: {
desc: '众多的贴心小工具,是你开发过程中召之即来的利器,让你飞镖在手,百步穿杨'
},
template: {
desc: '收集众多的常用页面和布局,减少开发者的重复工作,让你专注逻辑,事半功倍'
},
nav: {
components: '组件',
js: '工具',
template: '模板'
},
common: {
intro: '多平台快速开发的UI框架',
title: 'uview-plus',
},
}

7
common/mixin.js Normal file
View File

@@ -0,0 +1,7 @@
export default {
data() {
return {
isWeixin: true
}
}
}

2
common/props.js Normal file
View File

@@ -0,0 +1,2 @@
uni.$u.props.gap.bgColor = '#f3f4f6'
uni.$u.props.gap.height = '10'

1
common/province.js Normal file
View File

@@ -0,0 +1 @@
var provinceData=[{"label":"北京市","value":"11"},{"label":"天津市","value":"12"},{"label":"河北省","value":"13"},{"label":"山西省","value":"14"},{"label":"内蒙古自治区","value":"15"},{"label":"辽宁省","value":"21"},{"label":"吉林省","value":"22"},{"label":"黑龙江省","value":"23"},{"label":"上海市","value":"31"},{"label":"江苏省","value":"32"},{"label":"浙江省","value":"33"},{"label":"安徽省","value":"34"},{"label":"福建省","value":"35"},{"label":"江西省","value":"36"},{"label":"山东省","value":"37"},{"label":"河南省","value":"41"},{"label":"湖北省","value":"42"},{"label":"湖南省","value":"43"},{"label":"广东省","value":"44"},{"label":"广西壮族自治区","value":"45"},{"label":"海南省","value":"46"},{"label":"重庆市","value":"50"},{"label":"四川省","value":"51"},{"label":"贵州省","value":"52"},{"label":"云南省","value":"53"},{"label":"西藏自治区","value":"54"},{"label":"陕西省","value":"61"},{"label":"甘肃省","value":"62"},{"label":"青海省","value":"63"},{"label":"宁夏回族自治区","value":"64"},{"label":"新疆维吾尔自治区","value":"65"},{"label":"台湾","value":"66"},{"label":"香港","value":"67"},{"label":"澳门","value":"68"}];export default provinceData;

View File

@@ -0,0 +1,22 @@
<script>
export default {
name: "Footer"
}
</script>
<template>
<view class="footer">
<view>各位经销商</view>
<view>合作商请绑定公司名称与电话</view>
<view>并联系业务经理</view>
</view>
</template>
<style scoped lang="scss">
.footer {
color: #999;
margin: 50rpx auto;
text-align: center;
line-height: 50rpx;
}
</style>

View File

@@ -0,0 +1,57 @@
<script>
export default {
name: "HeaderSearch",
data() {
return {
keyword: ''
}
},
methods: {
goSearch() {
console.log('跳转')
this.$u.route({
url: '/pages/search/search',
params: {
keyword: this.keyword
}
})
},
onClickIcon() {
console.log('点击了扫码图标')
}
}
}
</script>
<template>
<view class="header-search">
<view class="left-search">
<up-search searchIcon="scan"
class="search"
placeholder="输入你想查询的服务"
v-model="keyword"
:showAction="false"
@focus="goSearch"
@clickIcon="onClickIcon"
></up-search>
</view>
</view>
</template>
<style scoped>
.header-search {
width: 100%;
padding: 29rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.left-search {
width: 100%;
}
.search {
//width: 400rpx;
}
</style>

View File

@@ -0,0 +1,43 @@
<script>
export default {
name: "SearchMain",
data() {
return {
keyword: ''
}
},
methods: {
searchList(value) {
console.log('搜索关键字22' + value)
},
onClickIcon() {
console.log('点击了扫码图标')
},
rightClick() {
console.log('rightClick');
},
leftClick() {
console.log('leftClick');
}
}
}
</script>
<template>
<view>
<up-navbar
title="个人中心"
@rightClick="rightClick"
:autoBack="true"
>
</up-navbar>
<up-search class="search" searchIcon="scan" placeholder="输入你想查询的服务"
@clickIcon="onClickIcon" v-model="keyword" :showAction="true" :animation="false" :actionStyle="{}" @custom="searchList"></up-search>
</view>
</template>
<style scoped>
.search {
width: 100%;
}
</style>

315
components/index/Index.vue Normal file
View File

@@ -0,0 +1,315 @@
<script>
import Tabs from "./Tabs.vue";
export default {
name: "Index",
components: {Tabs},
data() {
return {
swiper: [
{ image: 'https://cdn.uviewui.com/uview/swiper/swiper2.png' },
{ image: 'https://cdn.uviewui.com/uview/swiper/swiper1.png' },
{ image: 'https://cdn.uviewui.com/uview/swiper/swiper3.png' },
{ image: 'https://cdn.uviewui.com/uview/swiper/swiper1.png' },
{ image: 'https://cdn.uviewui.com/uview/swiper/swiper3.png' }
],
swiperHeight: 190, // 默认高度
quickAccess: [
{ name: '余额查询', icon: 'red-packet-fill' },
{ name: '积分商城', icon: 'integral' },
// { name: '客服中心', icon: 'kefup-ermai' },
// { name: '优惠券', icon: 'coupon-fill' },
// { name: '礼券中心', icon: 'gift-fill' },
{ name: '预约列表', icon: 'calendar-fill' },
{ name: '我的订单', icon: 'order' },
// { name: '我的住址', icon: 'map-fill' },
{ name: '我的收藏', icon: 'star-fill' },
// { name: '常见问题', icon: 'question-circle' },
// { name: '问题反馈', icon: 'error-circle-fill' },
],
scrollList: [
{
img: 'https://images.pexels.com/photos/9594679/pexels-photo-9594679.jpeg?auto=compress&cs=tinysrgb&w=600',
name: '全屋清洁',
price: '99.00',
tag: '热门',
purchased: true,
purchaseCount: 120
},
{
img: 'https://images.pexels.com/photos/9594679/pexels-photo-9594679.jpeg?auto=compress&cs=tinysrgb&w=600',
name: '全屋清洁',
price: '99.00',
tag: '热门',
purchased: true,
purchaseCount: 120
},
{
img: 'https://images.pexels.com/photos/9594679/pexels-photo-9594679.jpeg?auto=compress&cs=tinysrgb&w=600',
name: '全屋清洁',
price: '99.00',
tag: '热门',
purchased: true,
purchaseCount: 120
},
{
img: 'https://images.pexels.com/photos/9594679/pexels-photo-9594679.jpeg?auto=compress&cs=tinysrgb&w=600',
name: '全屋清洁',
price: '99.00',
tag: '热门',
purchased: true,
purchaseCount: 120
},
{
img: 'https://images.pexels.com/photos/9594679/pexels-photo-9594679.jpeg?auto=compress&cs=tinysrgb&w=600',
name: '全屋清洁',
price: '99.00',
tag: '热门',
purchased: true,
purchaseCount: 120
},
{
img: 'https://images.pexels.com/photos/9594679/pexels-photo-9594679.jpeg?auto=compress&cs=tinysrgb&w=600',
name: '全屋清洁',
price: '99.00',
tag: '热门',
purchased: true,
purchaseCount: 120
},
{
img: 'https://images.pexels.com/photos/9594679/pexels-photo-9594679.jpeg?auto=compress&cs=tinysrgb&w=600',
name: '全屋清洁',
price: '99.00',
tag: '热门',
purchased: true,
purchaseCount: 120
},
{
img: 'https://images.pexels.com/photos/9594679/pexels-photo-9594679.jpeg?auto=compress&cs=tinysrgb&w=600',
name: '深度清洁',
price: '129.00',
tag: '新品',
purchased: false,
purchaseCount: 80
},
{
img: 'https://images.pexels.com/photos/9594679/pexels-photo-9594679.jpeg?auto=compress&cs=tinysrgb&w=600',
name: '除尘服务',
price: '89.00',
tag: '优惠',
purchased: false,
purchaseCount: 200
},
{
img: 'https://images.pexels.com/photos/9594679/pexels-photo-9594679.jpeg?auto=compress&cs=tinysrgb&w=600',
name: '家庭保洁',
price: '119.00',
tag: '推荐',
purchased: true,
purchaseCount: 150
},
],
itemsPerPage: 10 // 每页显示的项数
};
},
computed: {
paginatedQuickAccess() {
const pages = [];
for (let i = 0; i < this.quickAccess.length; i += this.itemsPerPage) {
pages.push(this.quickAccess.slice(i, i + this.itemsPerPage));
}
return pages;
}
},
onLoad() {
console.log('onLoad');
},
mounted() {
this.updateSwiperHeight();
window.addEventListener('resize', this.updateSwiperHeight);
},
beforeDestroy() {
window.removeEventListener('resize', this.updateSwiperHeight);
},
methods: {
goToDetail(item) {
uni.navigateTo({
url: `/pages/info/index?name=${item.name}&price=${item.price}&img=${item.img}&tag=${item.tag}&purchaseCount=${item.purchaseCount}&purchased=${item.purchased}`
});
},
updateSwiperHeight() {
const width = window.innerWidth;
if (width >= 1920) {
this.swiperHeight = 600;
} else if (width >= 1024) {
this.swiperHeight = 500;
} else if (width >= 768) {
this.swiperHeight = 300;
} else {
this.swiperHeight = 190;
}
}
}
};
</script>
<template>
<view>
<!-- 轮播图 -->
<view class="swiper-images">
<up-swiper
:list="swiper"
keyName="image"
circular
:height="swiperHeight"
indicatorActiveColor="#ff8800"
indicatorInactiveColor="rgba(255, 255, 255, 0.35)"
:indicatorStyle="{ right: '20rpx' }"
:circular="true"
:indicator="true"
></up-swiper>
</view>
<!-- 快捷入口 -->
<view class="quick-access">
<view class="title">全部功能</view>
<swiper :indicator-dots="true" indicator-active-color="#ff8800" class="swiper">
<swiper-item v-for="(page, pageIndex) in paginatedQuickAccess" :key="pageIndex">
<up-grid :border="true" col="5">
<up-grid-item
v-for="(item, index) in page"
:key="index"
style="border: none !important;"
>
<up-icon
:customStyle="{paddingTop: 20 + 'rpx', color: '#ff8800'}"
:name="item.icon"
:size="22"
></up-icon>
<text class="grid-text">{{ item.name }}</text>
</up-grid-item>
</up-grid>
</swiper-item>
</swiper>
</view>
<view class="title">推荐服务</view>
<!-- 商品列表 -->
<view class="box">
<view class="lists">
<view class="h-item" v-for="(item, index) in scrollList" :key="index" @click="goToDetail(item)">
<view class="left">
<image :src="item.img" mode="aspectFill"></image>
</view>
<view class="info">
<view class="tit">
{{ item.name }}
<text v-if="item.purchased" class="purchased-tag">购买过</text>
</view>
<view class="price">价格: {{ item.price }}</view>
<view class="purchase-count">购买人数: {{ item.purchaseCount }}</view>
</view>
<view class="tag">{{ item.tag }}</view>
</view>
</view>
</view>
</view>
</template>
<style scoped>
.swiper-images {
margin: 20rpx auto;
}
.swiper {
border: none;
border-radius: 20rpx;
background-color: #ffffff;
box-shadow: 0 0 10rpx rgba(153, 153, 153, 0.3);
}
.grid-text {
font-size: 14px;
color: #909399;
padding: 10rpx 0 20rpx 0;
/* #ifndef APP-PLUS */
box-sizing: border-box;
/* #endif */
}
/* 服务列表 */
.box {
margin-top: 10px;
padding: 30rpx 50rpx 30rpx 10rpx;
background-color: #ffffff;
border-radius: 20rpx;
box-shadow: 0 0 10rpx rgba(153, 153, 153, 0.3);
}
.title {
font-size: 34rpx;
font-weight: 700;
margin-bottom: 20rpx;
margin-top: 30rpx;
}
.h-item {
width: 100%;
display: flex;
align-items: center;
margin-bottom: 10rpx;
padding: 20rpx;
border-radius: 10rpx;
box-shadow: 0 0 10rpx rgba(153, 153, 153, 0.1);
position: relative;
}
.h-item .left image {
width: 120rpx;
height: 120rpx;
border-radius: 10rpx;
margin-right: 20rpx;
}
.h-item .info {
flex: 1;
}
.h-item .info .tit {
font-size: 30rpx;
font-weight: 700;
margin-bottom: 10rpx;
}
.h-item .info .price {
font-size: 26rpx;
color: #999;
}
.h-item .info .purchase-count {
font-size: 24rpx;
color: #666;
margin-top: 10rpx;
position: absolute;
right: 10rpx;
}
.h-item .tag {
font-size: 24rpx;
color: #ff8800;
background-color: #ffe4d2;
padding: 5rpx 10rpx;
border-radius: 5rpx;
}
.purchased-tag {
font-size: 24rpx;
color: #fff;
background-color: #ff8800;
padding: 5rpx 10rpx;
border-radius: 5rpx;
margin-left: 10rpx;
}
</style>

44
components/index/Tabs.vue Normal file
View File

@@ -0,0 +1,44 @@
<script>
export default {
name: "Tabs",
data() {
return {
tabs: [
{
name: "推荐",
icon: "home-o"
},
{
name: "套餐",
icon: "apps-o"
},
{
name: "全屋清洁",
icon: "video-o"
},
]
}
},
methods: {
click(index) {
console.log(index);
}
}
}
</script>
<template>
<up-tabs :list="tabs"
@click="click"
:activeStyle="{
color: '#303133',
fontWeight: 'bold',
transform: 'scale(1.05)'
}"
lineColor="#f56c6c"
></up-tabs>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,227 @@
<template>
<view class="tab-bar-container">
<view class="tab-bar">
<view
v-for="(item, index) in list"
:key="index"
:class="['tab-item', { active: currentTab === item.pagePath, activity: item.pagePath === '/pages/activity/activity' }]"
@click="switchTab(item.pagePath)"
>
<view class="icon-wrapper" :class="{ active: currentTab === item.pagePath }">
<image :src="currentTab === item.pagePath ? item.selectedIconPath : item.iconPath" />
<view class="text">{{ item.text }}</view>
</view>
</view>
</view>
<view class="tab-bar-placeholder"></view>
</view>
</template>
<script>
export default {
props: {
selectedTab: {
type: String,
default: '/pages/home/home'
}
},
data() {
return {
currentTab: this.selectedTab,
list: [
{
text: '首页',
pagePath: '/pages/home/home',
iconPath: '/static/tabbar/home.png',
selectedIconPath: '/static/tabbar/home-fill.png'
},
{
text: '分类',
pagePath: '/pages/classify/classify',
iconPath: '/static/tabbar/classify.png',
selectedIconPath: '/static/tabbar/classify-fill.png'
},
{
text: '活动',
pagePath: '/pages/activity/activity',
iconPath: '/static/tabbar/activity.png',
selectedIconPath: '/static/tabbar/activity-fill.png'
},
{
text: '购物车',
pagePath: '/pages/shoppingCart/shoppingCart',
iconPath: '/static/tabbar/shopping-cart.png',
selectedIconPath: '/static/tabbar/shopping-cart-fill.png'
},
{
text: '我的',
pagePath: '/pages/mine/index',
iconPath: '/static/tabbar/mine.png',
selectedIconPath: '/static/tabbar/mine-fill.png'
}
]
};
},
created() {
this.updateCurrentTab();
},
mounted() {
uni.hideTabBar();
},
methods: {
switchTab(url) {
if (this.currentTab !== url) {
this.currentTab = url;
uni.switchTab({ url });
}
},
updateCurrentTab() {
const currentPage = getCurrentPages().pop();
if (currentPage && currentPage.route) {
this.currentTab = `/${currentPage.route}`;
}
}
},
watch: {
selectedTab(newVal) {
this.currentTab = newVal;
},
'$route'() {
this.updateCurrentTab();
}
},
onShow() {
this.updateCurrentTab();
}
};
</script>
<style scoped>
.tab-bar-container {
position: relative;
padding-bottom: env(safe-area-inset-bottom);
}
.tab-bar {
display: flex;
justify-content: space-around;
align-items: center;
background-color: #fff;
border-top: 1px solid #ddd;
height: 70px;
position: fixed;
bottom: 0;
width: 100%;
z-index: 999;
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
}
.tab-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
color: #888;
font-size: 12px;
position: relative;
}
.icon-wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: transform 0.3s;
}
.icon-wrapper.active {
transform: scale(1.2);
transition: transform 0.3s;
}
.tab-item image {
width: 24px;
height: 24px;
}
.tab-item .text {
margin-top: 3px;
color: #888;
}
.tab-item.active .text {
color: #ff8800;
}
.activity {
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
position: relative;
}
.activity .icon-wrapper {
position: relative;
top: -20px;
width: 70px;
height: 70px;
background: #fff;
border-radius: 50%;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
display: flex;
align-items: center;
justify-content: center;
}
.activity .icon-wrapper image {
width: 24px;
height: 24px;
}
.tab-bar-placeholder {
height: 80px;
}
.activity::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
height: 3px;
background: #ff8800;
z-index: -1;
}
.scale-enter-active, .scale-leave-active {
transition: transform 0.3s;
}
.scale-enter, .scale-leave-to /* .scale-leave-active in <2.1.8 */
{
transform: scale(1.2);
}
.scale-leave-active {
transform: scale(0.9);
}
.slide-left-enter-active, .slide-left-leave-active {
transition: transform 0.3s ease;
}
.slide-left-enter, .slide-left-leave-to {
transform: translateX(-100%);
}
.slide-right-enter-active, .slide-right-leave-active {
transition: transform 0.3s ease;
}
.slide-right-enter, .slide-right-leave-to {
transform: translateX(100%);
}
</style>

20
index.html Normal file
View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/main.js"></script>
</body>
</html>

25
main.js Normal file
View File

@@ -0,0 +1,25 @@
import App from './App'
// #ifndef VUE3
import Vue from 'vue'
import './uni.promisify.adaptor'
Vue.config.productionTip = false
App.mpType = 'app'
const app = new Vue({
...App
})
app.$mount()
// #endif
// main.js
import uviewPlus from 'uview-plus'
// #ifdef VUE3
import { createSSRApp } from 'vue'
export function createApp() {
const app = createSSRApp(App)
app.use(uviewPlus)
return {
app
}
}
// #endif

78
manifest.json Normal file
View File

@@ -0,0 +1,78 @@
{
"name" : "uview-shop",
"appid" : "__UNI__FC74E08",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : "100",
"transformPx" : false,
/* 5+App */
"app-plus" : {
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
},
/* */
"modules" : {},
/* */
"distribute" : {
/* android */
"android" : {
"permissions" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
/* ios */
"ios" : {},
/* SDK */
"sdkConfigs" : {},
"splashscreen" : {
"androidStyle" : "default"
}
}
},
/* */
"quickapp" : {},
/* */
"mp-weixin" : {
// "appid" : "wx679d36842570cea7", // 旧的铂尔曼
"appid" : "wx57b54060a579c31a", // 新的小程序
"setting" : {
"urlCheck" : false,
"minified" : true
},
// "resizable" : true,
"usingComponents" : true
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"uniStatistics" : {
"enable" : false
},
"vueVersion" : "3"
}

71
package-lock.json generated Normal file
View File

@@ -0,0 +1,71 @@
{
"name": "lgp-wx",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"clipboard": "^2.0.11",
"dayjs": "^1.11.13",
"js-base64": "^3.7.7",
"uview-plus": "^3.3.36"
}
},
"node_modules/clipboard": {
"version": "2.0.11",
"license": "MIT",
"dependencies": {
"good-listener": "^1.2.2",
"select": "^1.1.2",
"tiny-emitter": "^2.0.0"
}
},
"node_modules/dayjs": {
"version": "1.11.13",
"license": "MIT"
},
"node_modules/delegate": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
"integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==",
"license": "MIT"
},
"node_modules/good-listener": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
"integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==",
"license": "MIT",
"dependencies": {
"delegate": "^3.1.2"
}
},
"node_modules/js-base64": {
"version": "3.7.7",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz",
"integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==",
"license": "BSD-3-Clause"
},
"node_modules/select": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
"integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==",
"license": "MIT"
},
"node_modules/tiny-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==",
"license": "MIT"
},
"node_modules/uview-plus": {
"version": "3.3.36",
"dependencies": {
"clipboard": "^2.0.11",
"dayjs": "^1.11.3"
},
"engines": {
"HBuilderX": "^3.1.0"
}
}
}
}

8
package.json Normal file
View File

@@ -0,0 +1,8 @@
{
"dependencies": {
"clipboard": "^2.0.11",
"dayjs": "^1.11.13",
"js-base64": "^3.7.7",
"uview-plus": "^3.3.36"
}
}

112
pages.json Normal file
View File

@@ -0,0 +1,112 @@
{
"easycom": {
"autoscan": true,
"custom": {
"^u--(.*)": "uview-plus/components/u-$1/u-$1.vue",
"^up-(.*)": "uview-plus/components/u-$1/u-$1.vue",
"^u-([^-].*)": "uview-plus/components/u-$1/u-$1.vue"
}
},
"pages": [
{
"path": "pages/goods/index",
"style": {
"navigationBarTitleText": "家具优选"
}
},
{
"path": "pages/login/index",
"style": {
"navigationBarTitleText": "登录"
}
},
{
"path": "pages/mine/mine",
"style": {
"navigationBarTitleText": "我"
}
},
{
"path": "pages/my/index",
"style": {
"navigationBarTitleText": "个人信息"
}
},
{
"path": "pages/product/product",
"style": {
"navigationBarTitleText": "商品列表",
"enablePullDownRefresh": true,
"backgroundTextStyle": "dark",
"backgroundColor": "#f8f8f8",
"onReachBottomDistance": 50
}
},
{
"path": "pages/product/detail",
"style": {
"navigationBarTitleText": "商品详情",
"enablePullDownRefresh": false
}
},
{
"path": "pages/cart/cart",
"style": {
"navigationBarTitleText": "",
"enablePullDownRefresh": false
}
},
{
"path": "pages/cart/detail",
"style": {
"navigationBarTitleText": "清单详情",
"enablePullDownRefresh": false
}
},
{
"path": "pages/search/search",
"style": {
"navigationBarTitleText": "搜索",
"enablePullDownRefresh": false
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
"uniIdRouter": {},
"tabBar": {
"color": "#7A7E83",
"selectedColor": "#3cc51f",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/goods/index",
"text": "首页",
"iconPath": "static/tabbar/v2/home.png",
"selectedIconPath": "static/tabbar/v2/home-fill.png"
},
// {
// "pagePath": "pages/goods/index",
// "text": "首页",
// "iconPath": "static/tabbar/classify.png",
// "selectedIconPath": "static/tabbar/classify-fill.png"
// },
// {
// "pagePath": "pages/order/order",
// "text": "我的订单",
// "iconPath": "static/icon/order.png",
// "selectedIconPath": "static/icon/order.png"
// },
{
"pagePath": "pages/mine/mine",
"text": "我",
"iconPath": "static/tabbar/v2/mine.png",
"selectedIconPath": "static/tabbar/v2/mine-fill.png"
}
]
}
}

507
pages/cart/cart.vue Normal file
View File

@@ -0,0 +1,507 @@
<template>
<view class="container">
<!-- Header: 极简大气与详情页保持一致 -->
<view class="page-header">
<view class="header-content">
<view class="title-group">
<text class="header-title">我的清单</text>
<text class="header-subtitle">My COLLECTIONS</text>
</view>
<view class="header-badges">
<text class="count-badge">共计: {{ lists.length }}</text>
</view>
</view>
</view>
<!-- List Content -->
<scroll-view class="list-content" scroll-y @scrolltolower="loadMore">
<view v-if="lists.length > 0" class="list-wrapper">
<view
v-for="(item, index) in lists"
:key="index"
class="list-card animate-slide-in"
:style="{ animationDelay: index * 0.05 + 's' }"
@tap="goToDetail(item)"
>
<!-- 1. 卡片顶部工具栏日期 + 删除 -->
<!-- 彻底移除了 u-swipe-action解决编译报错 -->
<view class="card-top-bar">
<view class="top-left">
<u-icon name="calendar" color="#999" size="14"></u-icon>
<text class="date-text">{{ item.created_at }}</text>
</view>
<!-- 显性删除按钮更直观防误触 -->
<view class="delete-btn" @tap.stop="handleDelete(index, item.id)">
<u-icon name="trash" color="#ccc" size="18"></u-icon>
</view>
</view>
<!-- 2. 卡片主体内容 -->
<view class="card-body">
<view class="body-left">
<text class="item-name">{{ item.name }}</text>
<view class="item-remark" v-if="item.remark">
<text class="remark-line"></text>
<text class="remark-text">{{ item.remark }}</text>
</view>
<view class="item-remark placeholder" v-else>
<text class="remark-text">暂无备注信息</text>
</view>
</view>
<!-- 右侧数量展示更有设计感 -->
<view class="body-right">
<text class="count-label">商品数</text>
<text class="count-num">{{ item.count < 10 ? '0' + item.count : item.count }}</text>
</view>
</view>
<!-- 3. 底部装饰条 / 进入指引 -->
<view class="card-footer">
<text class="footer-status">查看详情</text>
<u-icon name="arrow-right" color="#B4854D" size="12"></u-icon>
</view>
</view>
</view>
<view class="empty-state" v-else>
<image src="/static/images/empty.png" mode="widthFix" class="empty-img"></image>
<text class="empty-text">您还没有创建任何清单</text>
<text class="empty-sub">点击下方按钮开始创建</text>
</view>
<!-- 底部垫高 -->
<view style="height: 180rpx;"></view>
</scroll-view>
<!-- Create Button (Floating) -->
<view class="fab-container">
<view class="create-btn" @tap="showCreateModal" hover-class="btn-hover">
<u-icon name="plus" color="#fff" size="18"></u-icon>
<text>新建清单</text>
</view>
</view>
<!-- Create List Modal -->
<u-modal
:show="showModal"
:title="false"
:showCancelButton="true"
cancelText="取消"
confirmText="创建"
confirmColor="#B4854D"
@confirm="createList"
@cancel="cancelCreate"
class="custom-modal"
>
<view class="modal-content">
<view class="modal-header-deco">
<text class="modal-title">新建清单</text>
<text class="modal-sub">创建新的选品组合</text>
</view>
<u-form :model="formData" ref="uForm" :errorType="['message']">
<u-form-item prop="name" class="modal-input-item">
<u-input
v-model="formData.name"
placeholder="清单名称"
border="bottom"
class="custom-input"
/>
</u-form-item>
<u-form-item prop="remark" class="modal-input-item">
<u-input
v-model="formData.remark"
placeholder="备注信息 (选填)"
border="bottom"
class="custom-input"
/>
</u-form-item>
</u-form>
</view>
</u-modal>
<!-- Loading -->
<u-loading-page :loading="isLoading" bgColor="rgba(255,255,255,0.9)" color="#B4854D" loadingText="加载中..."></u-loading-page>
</view>
</template>
<script>
import {createCartApi, deleteCartApi, getCartListApi} from "@/api/page/cart";
export default {
data() {
return {
isLoading: false,
showModal: false,
formData: {
name: '',
remark: ''
},
// swipeOptions 已移除,不再需要
lists: []
}
},
onShow() {
this.getList();
},
methods: {
navigateBack() {
uni.navigateBack()
},
showCreateModal() {
this.showModal = true
},
cancelCreate() {
this.showModal = false
this.formData = { name: '', remark: '' }
},
getList() {
getCartListApi().then((res) => {
this.lists = res
})
},
createList() {
if (!this.formData.name) {
uni.showToast({
title: '请输入清单名称',
icon: 'none'
})
return
}
createCartApi(this.formData).then((res) => {
this.showModal = false
this.formData = { name: '', remark: '' }
uni.showToast({
title: '已创建',
icon: 'success'
})
this.getList();
})
},
goToDetail(item) {
uni.navigateTo({
url: `/pages/cart/detail?id=${item.id}`
})
},
loadMore() {
// 暂无更多逻辑
},
handleDelete(index, id) {
uni.showModal({
title: '删除确认',
content: '确定要删除这个清单吗?此操作不可恢复。',
confirmText: '删除',
confirmColor: '#B4854D',
cancelColor: '#999',
success: (res) => {
if (res.confirm) {
deleteCartApi(id).then(() => {
this.lists.splice(index, 1)
uni.showToast({
title: '已删除',
icon: 'none'
})
})
}
}
})
}
}
}
</script>
<style lang="scss">
/* 核心Border-box 全局应用,防止布局错位 */
view, scroll-view, text, image {
box-sizing: border-box;
}
.container {
min-height: 100vh;
width: 100%;
background-color: #F7F8FA; /* 高级灰背景 */
display: flex;
flex-direction: column;
}
/* 动画 */
@keyframes slideInUp {
from { opacity: 0; transform: translateY(30rpx); }
to { opacity: 1; transform: translateY(0); }
}
.animate-slide-in {
animation: slideInUp 0.5s cubic-bezier(0.2, 0.8, 0.2, 1) backwards;
}
/* Header */
.page-header {
width: 100%;
background: #fff;
padding: 50rpx 40rpx 30rpx;
position: relative;
z-index: 10;
border-bottom: 1rpx solid rgba(0,0,0,0.03);
}
.header-content {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.header-title {
font-size: 48rpx;
font-weight: 300;
color: #1A1A1A;
letter-spacing: 2rpx;
margin-bottom: 8rpx;
display: block;
font-family: serif;
}
.header-subtitle {
font-size: 18rpx;
color: #B4854D;
letter-spacing: 6rpx;
font-weight: 600;
text-transform: uppercase;
display: block;
}
.count-badge {
font-size: 20rpx;
color: #999;
letter-spacing: 1rpx;
font-family: Arial, sans-serif;
}
/* List Content */
.list-content {
flex: 1;
width: 100%;
padding: 30rpx;
}
/* --- 核心卡片样式 --- */
.list-card {
background: #fff;
margin-bottom: 30rpx;
border-radius: 12rpx;
box-shadow: 0 10rpx 40rpx rgba(0,0,0,0.04);
overflow: hidden;
border: 1rpx solid rgba(0,0,0,0.02);
display: flex;
flex-direction: column;
}
/* Top Bar */
.card-top-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 30rpx;
border-bottom: 1rpx solid #F9F9F9;
}
.top-left {
display: flex;
align-items: center;
gap: 12rpx;
}
.date-text {
font-size: 22rpx;
color: #999;
letter-spacing: 1rpx;
font-family: Arial, sans-serif;
}
.delete-btn {
padding: 10rpx;
margin-right: -10rpx; /* 增加点击热区修正视觉位置 */
}
/* Card Body */
.card-body {
padding: 30rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.body-left {
flex: 1;
padding-right: 30rpx;
}
.item-name {
font-size: 36rpx;
font-weight: 500;
color: #222;
margin-bottom: 16rpx;
display: block;
font-family: serif;
}
.item-remark {
display: flex;
align-items: center;
gap: 10rpx;
}
.remark-line {
width: 20rpx;
height: 2rpx;
background-color: #B4854D;
}
.remark-text {
font-size: 24rpx;
color: #888;
font-style: italic;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 300rpx;
}
.placeholder .remark-text {
color: #ddd;
}
/* 右侧数量区 */
.body-right {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-left: 30rpx;
border-left: 1rpx dashed #eee;
}
.count-label {
font-size: 18rpx; /* 微调字体大小 */
color: #ccc;
letter-spacing: 2rpx;
margin-bottom: 4rpx;
}
.count-num {
font-size: 48rpx;
font-weight: 400; /* 数字细一点更高级 */
color: #B4854D;
font-family: 'Times New Roman', serif;
line-height: 1;
}
/* Card Footer */
.card-footer {
background: #FDFDFD;
padding: 16rpx 30rpx;
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1rpx solid #F9F9F9;
}
.footer-status {
font-size: 20rpx;
color: #B4854D;
letter-spacing: 2rpx;
font-weight: 600;
}
/* FAB */
.fab-container {
position: fixed;
bottom: 60rpx;
left: 0;
right: 0;
display: flex;
justify-content: center;
pointer-events: none;
z-index: 20;
}
.create-btn {
pointer-events: auto;
background: #222; /* 纯黑背景更显奢华 */
color: #B4854D; /* 金色文字 */
padding: 28rpx 70rpx;
border-radius: 60rpx;
display: flex;
align-items: center;
gap: 16rpx;
font-size: 26rpx;
font-weight: 600;
letter-spacing: 2rpx;
box-shadow: 0 20rpx 40rpx rgba(0,0,0,0.2);
transition: all 0.2s;
border: 1rpx solid #333;
}
.btn-hover {
transform: translateY(2rpx);
box-shadow: 0 10rpx 20rpx rgba(0,0,0,0.15);
}
/* Empty State */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 200rpx;
}
.empty-img {
width: 200rpx;
opacity: 0.8;
margin-bottom: 40rpx;
filter: grayscale(100%); /* 图片黑白化 */
}
.empty-text {
font-size: 30rpx;
color: #666;
font-weight: 500;
margin-bottom: 12rpx;
}
.empty-sub {
font-size: 24rpx;
color: #ccc;
letter-spacing: 1rpx;
}
/* Modal Styling - 极简风 */
.modal-content {
padding: 50rpx 30rpx 30rpx;
}
.modal-header-deco {
text-align: center;
margin-bottom: 60rpx;
}
.modal-title {
display: block;
font-size: 40rpx;
font-weight: 300;
color: #333;
letter-spacing: 4rpx;
margin-bottom: 10rpx;
}
.modal-sub {
display: block;
font-size: 24rpx;
color: #999;
}
.custom-input {
padding: 20rpx 0 !important;
}
</style>

428
pages/cart/detail.vue Normal file
View File

@@ -0,0 +1,428 @@
<template>
<view class="container">
<!-- 顶部 Header: 极简大气 -->
<view class="detail-header">
<view class="header-content">
<view class="title-group">
<text class="page-title">{{ listName }}</text>
<text class="page-subtitle">高端臻选系列</text>
</view>
<view class="header-badges">
<text class="count-badge">共计 {{ info.items ? info.items.length : 0 }} </text>
<text class="id-badge">清单号: {{ listId }}</text>
</view>
</view>
</view>
<!-- 商品列表 -->
<scroll-view class="list-container" scroll-y>
<view
v-for="(item, index) in info.items"
:key="index"
class="product-card animate-up"
:style="{ animationDelay: index * 0.05 + 's' }"
>
<!-- 1. 卡片顶部工具栏此处替代了 u-swipe-action -->
<!-- 彻底移除了滑动组件改为显性的删除按钮 -->
<view class="card-top-bar" @tap="navigateToDetail(item.product.id)">
<view class="top-left">
<text class="index-num">NO.{{ index + 1 < 10 ? '0' + (index + 1) : index + 1 }}</text>
<text class="divider-v">|</text>
<text class="status-text">已选配置</text>
</view>
<view class="top-right">
<!-- 删除按钮 -->
<view class="action-btn delete" @tap.stop="handleDelete(index, item.id)">
<u-icon name="trash" color="#999" size="18"></u-icon>
</view>
<!-- 详情跳转图标 -->
<view class="action-btn arrow">
<u-icon name="arrow-right" color="#ccc" size="14"></u-icon>
</view>
</view>
</view>
<!-- 2. 产品主体内容左图右文极简排版 -->
<view class="card-body" @tap="navigateToDetail(item.product.id)">
<view class="image-wrapper">
<image
:src="item.product.cover"
mode="aspectFill"
class="product-thumb"
/>
</view>
<view class="product-info">
<text class="p-title">{{ item.product.title }}</text>
<view class="p-tags">
<text class="luxury-tag">甄选家具</text>
</view>
</view>
</view>
<!-- 3. 规格清单轻奢菜单风格 -->
<view class="specs-panel">
<view class="specs-inner">
<view
v-for="(spec, sIndex) in item.product.price_sheet"
:key="sIndex"
class="spec-item"
>
<view class="spec-header">
<text class="spec-label">{{ spec.specification }}</text>
</view>
<view class="spec-detail-row">
<!-- 移除 nowrap 防止长文本撑开 -->
<text class="dim-text" v-if="spec.dimension">{{ spec.dimension }}</text>
<view class="dashed-line"></view>
<view class="price-group">
<view v-for="(pItem, pIndex) in spec.routine" :key="pIndex" class="price-unit">
<text class="currency">¥</text>{{ pItem }}
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<view class="safe-bottom"></view>
</scroll-view>
<u-toast ref="uToast"></u-toast>
</view>
</template>
<script>
import {deleteCartItemApi, getCartItemApi} from "@/api/page/cart";
export default {
data() {
return {
listId: '',
listName: '清单详情',
info: { items: [] },
// swipeOptions 已彻底移除
}
},
onLoad(options) {
this.listId = options.id
this.getInfo();
},
methods: {
handleDelete(index, id) {
uni.showModal({
title: '移除确认',
content: '将从您的甄选清单中移除此商品',
confirmText: '移除',
confirmColor: '#B4854D', // 金色确认按钮
cancelColor: '#999999',
success: (res) => {
if (res.confirm) {
deleteCartItemApi(id).then(() => {
this.info.items.splice(index, 1)
this.$refs.uToast.show({
type: 'success',
message: '已移除'
})
})
}
}
})
},
getInfo() {
uni.showLoading({ title: '加载中...' }) // 中文Loading
getCartItemApi(this.listId).then((res) => {
this.info = res || { items: [] };
uni.hideLoading()
})
},
navigateToDetail(id) {
uni.navigateTo({
url: `/pages/product/detail?id=${id}`,
})
}
}
}
</script>
<style lang="scss">
/* 核心修复:强制使用 border-box 盒模型,防止 padding 撑开宽度 */
view, scroll-view, text, image {
box-sizing: border-box;
}
/* 全局容器:高级灰背景 */
.container {
min-height: 100vh;
width: 100%; /* 确保容器宽度 */
background-color: #F7F8FA;
display: flex;
flex-direction: column;
overflow-x: hidden; /* 防止水平溢出 */
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(30rpx); }
to { opacity: 1; transform: translateY(0); }
}
.animate-up { animation: slideUp 0.5s cubic-bezier(0.2, 0.8, 0.2, 1) backwards; }
/* 1. Header 区域:极简,大量留白 */
.detail-header {
width: 100%;
background: #fff;
padding: 50rpx 40rpx 30rpx;
position: relative;
z-index: 10;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.title-group {
display: flex;
flex-direction: column;
}
.page-title {
font-size: 48rpx;
font-weight: 300; /* 细字体显得更高级 */
color: #1A1A1A;
letter-spacing: 2rpx;
margin-bottom: 8rpx;
font-family: serif; /* 尝试使用衬线风格(取决于系统支持) */
}
.page-subtitle {
font-size: 18rpx;
color: #B4854D; /* 金色副标题 */
letter-spacing: 6rpx;
font-weight: 600;
text-transform: uppercase;
}
.header-badges {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 6rpx;
}
.count-badge, .id-badge {
font-size: 20rpx;
color: #999;
font-family: Arial, Helvetica, sans-serif;
letter-spacing: 1rpx;
}
/* 2. 列表容器 */
.list-container {
flex: 1;
width: 100%; /* 明确宽度 */
padding: 30rpx;
}
/* 3. 商品卡片:核心设计 */
.product-card {
width: 100%; /* 确保卡片不超出父容器 */
background: #fff;
margin-bottom: 30rpx;
border-radius: 12rpx; /* 不需要太大的圆角 */
box-shadow: 0 10rpx 40rpx rgba(0,0,0,0.04); /* 大而柔和的阴影 */
overflow: hidden;
position: relative;
border: 1rpx solid rgba(0,0,0,0.02); /* 极细的边框 */
}
/* -- 卡片顶部工具栏 -- */
.card-top-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 30rpx;
border-bottom: 1rpx solid #F5F5F5;
background: #fff;
}
.top-left {
display: flex;
align-items: center;
}
.index-num {
font-family: 'Times New Roman', serif;
font-size: 28rpx;
font-weight: 700;
color: #333;
font-style: italic;
}
.divider-v {
margin: 0 16rpx;
color: #eee;
font-size: 20rpx;
}
.status-text {
font-size: 22rpx;
color: #999;
letter-spacing: 1rpx;
}
.top-right {
display: flex;
align-items: center;
gap: 30rpx; /* 按钮之间拉开距离 */
}
.action-btn {
display: flex;
align-items: center;
justify-content: center;
width: 44rpx;
height: 44rpx;
}
/* -- 卡片主体 (图片+标题) -- */
.card-body {
padding: 30rpx;
display: flex;
align-items: center; /* 垂直居中 */
gap: 30rpx;
}
.image-wrapper {
width: 140rpx;
height: 140rpx;
border-radius: 6rpx;
overflow: hidden;
background: #F9F9F9;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05);
flex-shrink: 0; /* 防止图片被压缩 */
}
.product-thumb {
width: 100%;
height: 100%;
}
.product-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
overflow: hidden; /* 防止文字过长溢出 */
}
.p-title {
font-size: 32rpx;
color: #222;
font-weight: 500;
line-height: 1.5;
margin-bottom: 16rpx;
/* 增加截断,防止超长标题撑开 */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.p-tags {
display: flex;
}
.luxury-tag {
font-size: 18rpx;
color: #B4854D;
background: rgba(180, 133, 77, 0.08); /* 极淡的金色背景 */
padding: 4rpx 12rpx;
border-radius: 4rpx;
letter-spacing: 1rpx;
font-weight: 600;
}
/* -- 规格清单面板 (菜单式) -- */
.specs-panel {
padding: 0 30rpx 40rpx; /* 底部留白 */
}
.specs-inner {
background: #FAFAFA; /* 浅灰底 */
border: 1rpx solid #EFEFEF;
border-radius: 8rpx;
padding: 24rpx;
}
.spec-item {
margin-bottom: 24rpx;
}
.spec-item:last-child {
margin-bottom: 0;
}
.spec-header {
margin-bottom: 8rpx;
}
.spec-label {
font-size: 26rpx;
color: #333;
font-weight: 600;
display: block;
}
.spec-detail-row {
display: flex;
align-items: baseline;
justify-content: space-between;
}
.dim-text {
font-size: 22rpx;
color: #888;
/* 移除 nowrap允许换行防止长尺寸描述撑开容器 */
/* white-space: nowrap; */
padding-right: 10rpx;
}
/* 虚线连接符 */
.dashed-line {
flex: 1;
height: 1px;
border-bottom: 2rpx dashed #E0E0E0;
margin: 0 16rpx;
position: relative;
top: -6rpx; /* 微调对齐 */
min-width: 40rpx; /* 保证至少有一点虚线 */
}
.price-group {
display: flex;
flex-direction: column;
align-items: flex-end;
flex-shrink: 0; /* 防止价格被挤压 */
}
.price-unit {
font-family: 'Times New Roman', serif; /* 衬线体价格,显贵 */
font-size: 30rpx;
color: #B4854D;
font-weight: bold;
}
.currency {
font-size: 20rpx;
margin-right: 4rpx;
font-weight: normal;
}
.safe-bottom {
height: 60rpx;
}
</style>

264
pages/goods/index.vue Normal file
View File

@@ -0,0 +1,264 @@
<template>
<view class="container">
<!-- 轮播图优化 -->
<view class="hero-banner">
<swiper class="product-swiper" circular autoplay interval="5000" duration="800">
<swiper-item v-for="(item, index) in carousel" :key="index" class="swiper-item-box">
<view class="image-wrapper">
<image :src="item.url" mode="aspectFill" class="swiper-image" />
<view class="swiper-overlay"></view>
</view>
</swiper-item>
</swiper>
</view>
<!-- Search Bar (悬浮毛玻璃风格) -->
<view class="search-wrapper">
<view class="search-box">
<up-search
type="text"
placeholder="搜索心仪的产品型号"
placeholder-class="search-placeholder"
v-model="title"
:showAction="true"
:animation="true"
actionText="搜索"
:disabled="true"
bgColor="#f5f5f5"
@search="handleSearch"
@click="handleSearch"
/>
</view>
</view>
<!-- 分类展示优化 (回归三列布局) -->
<view class="category-wrapper">
<view class="section-header animate-fade-in-up">
<text class="title-en">CATEGORIES</text>
<text class="title-cn">产品分类</text>
</view>
<view class="category-grid">
<view
v-for="(item, index) in categories"
:key="index"
class="category-card animate-fade-in-up"
:style="{ animationDelay: index * 0.05 + 's' }"
@tap="navigateToList(item.id)"
>
<view class="category-image-box">
<image :src="item.url" mode="aspectFill" class="cat-img"></image>
<view class="cat-mask"></view>
</view>
<view class="category-info">
<text class="category-name">{{ item.name }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import {getCarouselApi, getCategoryListApi} from "@/api/page/home";
import {getCache} from "@/utils/cache";
export default {
data() {
return {
categoriesList: [],
categories: [],
carousel: [],
title: ""
}
},
onShow() {
const userInfo = getCache('user_info')
if (userInfo) {
uni.setNavigationBarTitle({
title: userInfo?.enterprise.name || '家具优选'
})
}
},
onShareAppMessage() {
const userInfo = getCache('user_info')
return {
title: userInfo?.enterprise.name || '家具优选',
path: `pages/goods/index`,
imageUrl: 'http://qiniu.boerman.top/b_a2d78f027b3d26c0b0621655726c0c54.jpg'
};
},
mounted() {
this.getCarousel();
this.getCategoryList();
},
methods: {
navigateToList(pid) {
uni.navigateTo({
url: `/pages/product/product?pid=${pid}`
})
},
getCarousel() {
getCarouselApi().then((res) => {
this.carousel = res;
});
},
getCategoryList() {
getCategoryListApi().then((res) => {
this.categories = res;
});
},
handleSearch() {
uni.navigateTo({
url: `/pages/search/search?title=${this.title}`
})
}
}
}
</script>
<style lang="scss">
@keyframes fadeInUp {
from { opacity: 0; transform: translate3d(0, 40rpx, 0); }
to { opacity: 1; transform: translate3d(0, 0, 0); }
}
.animate-fade-in-up {
animation: fadeInUp 0.8s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
opacity: 0;
}
.container {
min-height: 100vh;
background-color: #F7F8FA;
}
.search-wrapper {
position: sticky;
top: 0;
z-index: 100;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
padding: 20rpx 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.03);
}
.search-box {
border-radius: 40rpx;
overflow: hidden;
}
.hero-banner {
position: relative;
height: 400rpx;
margin-top: 20rpx;
padding: 0 20rpx;
}
.product-swiper {
height: 100%;
border-radius: 20rpx;
overflow: hidden;
transform: translateZ(0);
}
.image-wrapper {
width: 100%;
height: 100%;
position: relative;
}
.swiper-image {
width: 100%;
height: 100%;
}
.swiper-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(to bottom, rgba(0,0,0,0) 80%, rgba(0,0,0,0.3) 100%);
}
.category-wrapper {
padding: 40rpx 20rpx;
}
.section-header {
margin-bottom: 30rpx;
padding-left: 10rpx;
display: flex;
flex-direction: column;
}
.title-en {
font-size: 20rpx;
color: #B4854D;
letter-spacing: 4rpx;
font-weight: 700;
margin-bottom: 4rpx;
text-transform: uppercase;
}
.title-cn {
font-size: 36rpx;
color: #333;
font-weight: 600;
letter-spacing: 2rpx;
}
/* 3列布局 */
.category-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20rpx;
}
.category-card {
background: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.03);
position: relative;
transition: transform 0.2s;
}
.category-card:active {
transform: scale(0.97);
}
.category-image-box {
height: 180rpx;
width: 100%;
position: relative;
}
.cat-img {
width: 100%;
height: 100%;
}
.cat-mask {
position: absolute;
inset: 0;
background: rgba(0,0,0,0.02);
}
.category-info {
padding: 16rpx 10rpx;
text-align: center;
background: #fff;
}
.category-name {
font-size: 26rpx;
color: #333;
font-weight: 500;
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>

183
pages/login/index.vue Normal file
View File

@@ -0,0 +1,183 @@
<template>
<view class="login-container">
<view class="logo-area">
<image
src="/static/images/logo.png"
mode="aspectFill"
class="logo-image"
/>
<text class="logo-text">佛山家具工厂</text>
</view>
<view class="login-form">
<view class="form-title">微信一键登录</view>
<button
class="wechat-login-btn"
@click="handleAuth"
>
<u-icon name="weixin-fill" color="#ffffff" size="28"></u-icon>
<text>一键登录</text>
</button>
</view>
<u-toast ref="uToast"></u-toast>
</view>
</template>
<script>
import {loginApi} from "@/api/page/auth";
import {getCache, setCache} from "@/utils/cache";
export default {
data() {
return {
phoneCode: '',
}
},
methods: {
handleAuth() {
// loading
this.$refs.uToast.show({
type: 'loading',
message: '正在登录...'
})
// 获取用户信息
uni.login({
provider: 'weixin',
success: loginRes => {
if (loginRes.code) {
loginApi(
loginRes.code,
this.phoneCode,
'',
'微信用户' + Math.random().toString(36).substring(2)
).then((result) => {
console.log('登录成功', result);
setCache('token', result.token)
setCache('user_info', result.user_info)
uni.showToast({
title: '登录成功',
icon: 'success'
})
uni.hideLoading()
setTimeout(() => {
let url = getCache('currentPage')
if (url) {
setCache('currentPage', '')
} else {
url = '/pages/goods/index'
}
uni.reLaunch({
url: url
})
}, 1000)
})
} else {
console.error('登录失败!' + loginRes.errMsg);
}
}
});
}
}
}
</script>
<style lang="scss">
.login-container {
min-height: 100vh;
background-color: #ffffff;
padding: 60rpx 40rpx;
}
.logo-area {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 80rpx;
}
.logo-image {
width: 180rpx;
height: 180rpx;
border-radius: 24rpx;
margin-bottom: 20rpx;
}
.logo-text {
font-size: 36rpx;
color: #333333;
font-weight: 600;
}
.login-form {
padding: 0 20rpx;
}
.form-title {
font-size: 40rpx;
color: #333333;
font-weight: 600;
margin-bottom: 60rpx;
text-align: center;
}
.wechat-login-btn {
width: 100%;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #B4854D 0%, #8C6239 100%);
color: #ffffff;
font-size: 32rpx;
font-weight: 500;
border-radius: 44rpx;
margin: 60rpx 0;
box-shadow: 0 4rpx 12rpx rgba(180, 133, 77, 0.2);
&:active {
transform: scale(0.98);
opacity: 0.9;
}
text {
margin-left: 12rpx;
}
}
.agreement {
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
color: #666666;
}
.agreement-text {
margin-left: 8rpx;
}
.link {
color: #B4854D;
}
.auth-content {
padding: 30rpx;
}
.auth-title {
font-size: 32rpx;
font-weight: 600;
color: #333333;
margin-bottom: 20rpx;
text-align: center;
}
.auth-text {
font-size: 28rpx;
color: #666666;
line-height: 1.5;
}
</style>

360
pages/mine/mine.vue Normal file
View File

@@ -0,0 +1,360 @@
<template>
<view class="page-container">
<!-- 顶部背景 -->
<view class="header-bg"></view>
<view class="content">
<!-- 用户信息卡片 -->
<view class="user-card animate-slide-down">
<view class="user-info-row" @click="handleUserClick">
<view class="avatar-wrapper">
<image
:src="userInfo.avatar || 'https://img2.baidu.com/it/u=2953585264,744730101&fm=253&fmt=auto&app=138&f=JPEG?w=360&h=360'"
mode="aspectFill"
class="avatar"
/>
</view>
<view class="info-text">
<view class="nickname">{{ userInfo.nick_name || '登录 / 注册' }}</view>
<view class="subtitle" v-if="userInfo.id">ID: {{ userInfo.id }}</view>
<view class="subtitle" v-else>点击登录开启家具选购之旅</view>
</view>
<view class="arrow-icon">
<u-icon name="arrow-right" color="#ccc" size="16"></u-icon>
</view>
</view>
<!-- 统计数据 -->
<view class="stats-row">
<view class="stat-item">
<!-- 确保 count 字段正确绑定 -->
<view class="num">{{ count }}</view>
<view class="label">当前清单数量</view>
</view>
<view class="vertical-line"></view>
<view class="stat-item" @click="goCart">
<view class="action-text">去查看</view>
</view>
</view>
</view>
<!-- 企业卡片 -->
<view v-if="userInfo && userInfo.enterprise_id" class="enterprise-card animate-fade-in" @click="goEnterpriseDetail">
<image class="ent-logo" :src="userInfo.enterprise?.logo" mode="aspectFill"></image>
<view class="ent-info">
<text class="ent-name">{{ userInfo.enterprise?.name }}</text>
<text class="ent-tag">已绑定企业账户</text>
</view>
<u-icon name="arrow-right" color="rgba(255,255,255,0.6)" size="14"></u-icon>
</view>
<!-- 菜单列表 -->
<view class="menu-group animate-fade-in">
<view class="menu-item" @click="goCart">
<view class="item-left">
<image src="/static/icon/await.png" class="menu-icon" mode="aspectFit"></image>
<text>我的清单</text>
</view>
<u-icon name="arrow-right" color="#ccc" size="14"></u-icon>
</view>
<view class="menu-item" @click="goMy">
<view class="item-left">
<u-icon name="setting-fill" color="#B4854D" size="20" class="u-icon-fix"></u-icon>
<text style="margin-left: 20rpx;">个人设置</text>
</view>
<u-icon name="arrow-right" color="#ccc" size="14"></u-icon>
</view>
</view>
<Footer />
</view>
</view>
</template>
<script>
import CustomTabBar from "../../components/tabbar/customTabbar.vue";
import Footer from "../../components/common/Footer.vue";
import {getCache} from "@/utils/cache";
import {getUserInfoApi} from "@/api/page/user";
export default {
components: { Footer, CustomTabBar },
data() {
return {
userInfo: {},
count: 0,
indexList: [
// {
// name: '用户反馈',
// icon: {
// color: '#ff8800',
// size: '26',
// type: 'info-circle'
// },
// page: '/pages/feedback/feedback'
// },
// {
// name: '我的邮件',
// icon: {
// color: '#ff8800',
// size: '26',
// type: 'email'
// },
// page: '/pages/email/email'
// },
// {
// name: '分享有礼',
// icon: {
// color: '#ff8800',
// size: '26',
// type: 'gift'
// },
// page: '/pages/share/share'
// },
],
};
},
onShow() {
this.checkLoginAndLoad();
},
methods: {
checkLoginAndLoad() {
// 优先从缓存读取以快速渲染
// const cachedUser = getCache('user_info');
// if (cachedUser) {
// this.userInfo = cachedUser;
// }
// 调用接口获取最新数据包括count
this.getMyInfo();
},
getMyInfo() {
getUserInfoApi().then((res) => {
console.log("UserInfo API Response:", res);
// 强制确保 count 赋值
this.count = res.count;
this.userInfo = res.user;
}).catch(err => {
console.error("Get user info failed:", err);
});
},
handleUserClick() {
if (!this.userInfo.id) {
this.loginFun();
} else {
this.goMy();
}
},
loginFun() {
uni.navigateTo({ url: '/pages/login/index' });
},
goMy() {
uni.navigateTo({ url: '/pages/my/index' });
},
goCart() {
uni.navigateTo({ url: '/pages/cart/cart' });
},
goEnterpriseDetail() {
// 企业详情跳转逻辑
}
},
}
</script>
<style lang="scss">
.page-container {
min-height: 100vh;
background-color: #F5F7FA;
position: relative;
}
.header-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 350rpx;
background: linear-gradient(135deg, #E6D5C1 0%, #F5F7FA 100%);
z-index: 0;
border-radius: 0 0 40rpx 40rpx;
}
.content {
position: relative;
z-index: 1;
padding: 140rpx 30rpx 40rpx;
}
/* User Card */
.user-card {
background: #fff;
border-radius: 24rpx;
padding: 40rpx 30rpx;
box-shadow: 0 8rpx 24rpx rgba(180, 133, 77, 0.08);
margin-bottom: 24rpx;
}
.user-info-row {
display: flex;
align-items: center;
margin-bottom: 40rpx;
}
.avatar-wrapper {
margin-right: 24rpx;
}
.avatar {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
border: 4rpx solid #fff;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1);
background: #f0f0f0;
}
.info-text {
flex: 1;
}
.nickname {
font-size: 36rpx;
font-weight: 700;
color: #333;
margin-bottom: 8rpx;
}
.subtitle {
font-size: 24rpx;
color: #999;
}
.stats-row {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 30rpx;
border-top: 1rpx solid #f5f5f5;
}
.stat-item {
flex: 1;
text-align: center;
}
.num {
font-size: 44rpx;
font-weight: 700;
color: #B4854D;
line-height: 1.2;
}
.label {
font-size: 24rpx;
color: #999;
margin-top: 4rpx;
}
.vertical-line {
width: 2rpx;
height: 40rpx;
background: #eee;
}
.action-text {
display: inline-block;
padding: 12rpx 32rpx;
background: #333;
color: #D4AF37;
font-size: 24rpx;
border-radius: 30rpx;
}
/* Enterprise Card */
.enterprise-card {
background: linear-gradient(135deg, #2c2c2c 0%, #4a4a4a 100%);
border-radius: 20rpx;
padding: 30rpx;
display: flex;
align-items: center;
margin-bottom: 24rpx;
box-shadow: 0 8rpx 20rpx rgba(0,0,0,0.15);
}
.ent-logo {
width: 80rpx;
height: 80rpx;
border-radius: 12rpx;
background: #fff;
margin-right: 20rpx;
}
.ent-info {
flex: 1;
display: flex;
flex-direction: column;
}
.ent-name {
color: #ECD1A8;
font-size: 30rpx;
font-weight: 600;
margin-bottom: 6rpx;
}
.ent-tag {
color: rgba(236, 209, 168, 0.6);
font-size: 22rpx;
}
/* Menu Group */
.menu-group {
background: #fff;
border-radius: 24rpx;
padding: 10rpx 0;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.02);
}
.menu-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx 40rpx;
transition: background 0.2s;
}
.menu-item:active {
background: #f9f9f9;
}
.item-left {
display: flex;
align-items: center;
font-size: 30rpx;
color: #333;
}
.menu-icon {
width: 40rpx;
height: 40rpx;
margin-right: 20rpx;
}
.u-icon-fix {
width: 40rpx;
text-align: center;
}
/* Animations */
@keyframes slideDown {
from { opacity: 0; transform: translateY(-20rpx); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.animate-slide-down { animation: slideDown 0.6s cubic-bezier(0.2, 0.8, 0.2, 1); }
.animate-fade-in { animation: fadeIn 0.8s ease-out; }
</style>

225
pages/my/index.vue Normal file
View File

@@ -0,0 +1,225 @@
<template>
<view class="page-container">
<view class="content-wrapper animate-enter">
<view class="header">
<text class="title">个人信息</text>
<text class="subtitle">维护您的基本资料</text>
</view>
<!-- 信息展示列表 -->
<view class="info-list">
<!-- 昵称 -->
<view class="info-item" @tap="openNicknameModal">
<view class="item-left">
<text class="label">用户昵称</text>
</view>
<view class="item-right">
<text class="value">{{ nickname || '未设置' }}</text>
<u-icon name="arrow-right" color="#ccc" size="14"></u-icon>
</view>
</view>
<view class="divider"></view>
<!-- 手机号 -->
<view class="info-item" @tap="openPhoneModal">
<view class="item-left">
<text class="label">手机号码</text>
</view>
<view class="item-right">
<text class="value">{{ phone || '未绑定' }}</text>
<u-icon name="arrow-right" color="#ccc" size="14"></u-icon>
</view>
</view>
</view>
<view class="footer-action">
<button class="logout-btn" @click="logout" hover-class="btn-hover">
退出登录
</button>
</view>
</view>
<!-- Modal: 修改昵称 -->
<u-modal
:show="showNicknameModal"
title="修改昵称"
:showCancelButton="true"
cancelText="取消"
confirmText="保存"
confirmColor="#B4854D"
@cancel="showNicknameModal = false"
@confirm="saveNickname"
>
<view class="modal-input-wrapper">
<u-input
v-model="tempNickname"
placeholder="请输入新昵称"
border="surround"
clearable
/>
</view>
</u-modal>
<!-- Modal: 绑定手机 (手动输入) -->
<u-modal
:show="showPhoneModal"
title="绑定手机号"
:showCancelButton="true"
cancelText="取消"
confirmText="保存"
confirmColor="#B4854D"
@cancel="showPhoneModal = false"
@confirm="savePhone"
>
<view class="modal-input-wrapper">
<u-input
v-model="tempPhone"
placeholder="请输入手机号码"
type="number"
border="surround"
clearable
/>
<view class="tip-text">请确保输入真实的手机号码</view>
</view>
</u-modal>
<u-toast ref="uToast"></u-toast>
</view>
</template>
<script>
import { bandPhoneApi, updateNickNameApi, getUserInfoApi } from "@/api/page/user";
import {getCache, setCache} from "@/utils/cache";
export default {
data() {
return {
nickname: '',
phone: '',
// Modal States
showNicknameModal: false,
tempNickname: '',
showPhoneModal: false,
tempPhone: '',
}
},
onLoad() {
this.initUserInfo()
},
methods: {
async initUserInfo() {
try {
const res = await getUserInfoApi();
if(res.user) {
this.nickname = res.user.nick_name;
this.phone = res.user.phone;
}
} catch (error) {
console.error(error)
}
},
openNicknameModal() {
this.tempNickname = this.nickname;
this.showNicknameModal = true;
},
openPhoneModal() {
this.tempPhone = this.phone;
this.showPhoneModal = true;
},
async saveNickname() {
if(!this.tempNickname.trim()) {
this.showNicknameModal = false;
return this.$refs.uToast.show({ type: 'error', message: '昵称不能为空' });
}
try {
updateNickNameApi(this.tempNickname).then(() => {
this.nickname = this.tempNickname;
this.updateCache('nick_name', this.nickname);
this.showNicknameModal = false;
this.$refs.uToast.show({ type: 'success', message: '保存成功' });
});
} catch (error) {
this.showNicknameModal = false;
}
},
// 绑定手动输入的手机号
async savePhone() {
if(!this.tempPhone || this.tempPhone.length < 11) {
this.showPhoneModal = false;
return this.$refs.uToast.show({ type: 'error', message: '请输入有效的手机号' });
}
try {
bandPhoneApi(this.tempPhone).then(() => {
this.phone = this.tempPhone;
this.updateCache('phone', this.phone);
this.showPhoneModal = false;
this.$refs.uToast.show({ type: 'success', message: '绑定成功' });
});
} catch (error) {
this.showPhoneModal = false;
this.$refs.uToast.show({ type: 'error', message: '绑定失败' });
}
},
updateCache(key, value) {
let userInfo = getCache('user_info') || {};
userInfo[key] = value;
setCache('user_info', userInfo);
},
logout() {
uni.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
uni.removeStorageSync('token');
uni.removeStorageSync('user_info');
uni.reLaunch({
url: '/pages/login/index'
});
}
}
});
},
}
}
</script>
<style lang="scss">
.page-container {
min-height: 100vh;
background-color: #F7F8FA;
padding: 30rpx;
}
.animate-enter { animation: slideUp 0.6s ease-out; }
@keyframes slideUp { from { opacity: 0; transform: translateY(30rpx); } to { opacity: 1; transform: translateY(0); } }
.header { margin-bottom: 40rpx; padding-left: 20rpx; }
.title { font-size: 40rpx; font-weight: 700; color: #333; margin-bottom: 8rpx; display: block; }
.subtitle { font-size: 26rpx; color: #999; }
/* Info List */
.info-list { background: #fff; border-radius: 24rpx; padding: 0 30rpx; box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.03); }
.info-item { display: flex; justify-content: space-between; align-items: center; height: 120rpx; }
.divider { height: 1rpx; background: #f5f5f5; }
.item-left .label { font-size: 30rpx; color: #333; font-weight: 500; }
.item-right { display: flex; align-items: center; gap: 16rpx; }
.item-right .value { font-size: 28rpx; color: #666; }
.modal-input-wrapper { padding: 30rpx 10rpx; }
.tip-text { font-size: 24rpx; color: #999; margin-top: 20rpx; text-align: center; }
.footer-action { margin-top: 60rpx; }
.logout-btn { background: #fff; color: #ff4444; font-size: 30rpx; height: 88rpx; border-radius: 44rpx; box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05); }
</style>

471
pages/product/detail.vue Normal file
View File

@@ -0,0 +1,471 @@
<template>
<view class="container">
<!-- 产品轮播图 -->
<view class="swiper-container">
<swiper class="product-swiper" indicator-dots indicator-active-color="#B4854D" circular>
<swiper-item v-for="(image, index) in product.carousel" :key="index">
<image :src="image" mode="aspectFill" @click="previewImages(image)" class="product-image" />
</swiper-item>
</swiper>
</view>
<!-- 悬浮分享按钮 (Fixed Positioning) -->
<button class="share-fixed-btn animate-scale" open-type="share" hover-class="none">
<u-icon name="share-fill" color="#333" size="20"></u-icon>
</button>
<!-- 产品信息卡片 (移除负边距防止遮挡) -->
<view class="info-card animate-slide-up">
<view class="title-section">
<text class="product-code">{{ product.title }}</text>
<view class="product-alias animate-fade-in" v-if="product.alias">
<rich-text :nodes="product.alias"></rich-text>
</view>
</view>
<!-- 规格表格 -->
<view class="specs-section">
<view class="section-title">规格参数</view>
<view class="specs-table">
<view :class="['specs-header', product.is_show_price ? 'cols-2' : 'cols-1']">
<text>规格 / 尺寸</text>
<text v-if="product.is_show_price">报价信息</text>
</view>
<view
v-for="(spec, index) in product.price_sheet"
:key="index"
:class="['specs-row', product.is_show_price ? 'cols-2' : 'cols-1']"
>
<view class="cell-spec">
<view class="spec-name">{{ spec.specification }}</view>
<view class="spec-dim">{{ spec.dimension }}</view>
</view>
<view class="cell-price" v-if="product.is_show_price">
<view v-for="(item, idx) in spec.routine" :key="idx" class="price-tag">
{{ item }}
</view>
</view>
</view>
</view>
</view>
<!-- 详情图片 -->
<view class="detail-section">
<view class="section-title">产品细节</view>
<view class="detail-images">
<image
v-for="(image, index) in product.carousel"
:key="index"
:src="image"
@click="previewImages(image)"
mode="widthFix"
class="detail-image"
/>
</view>
</view>
</view>
<!-- 添加清单弹窗 -->
<up-popup
:show="listModal"
title="添加到清单"
mode="bottom"
:round="24"
@close="listModal = false"
:safeAreaInsetTop="true"
:closeable="true"
bgColor="#F7F8FA"
>
<view class="popup-content">
<scroll-view scroll-y style="max-height: 600rpx;">
<view class="list-options">
<view
v-for="(item, index) in listOption"
:key="index"
class="option-item"
@click="addToCart(item.id)"
>
<view class="option-info">
<text class="option-name">{{ item.name }}</text>
<text class="option-id">ID: {{ item.id }}</text>
</view>
<view class="add-icon-btn">
<u-icon name="plus" color="#fff" size="14"></u-icon>
</view>
</view>
</view>
</scroll-view>
</view>
</up-popup>
<!-- 底部垫高 -->
<view class="safe-padding-bottom"></view>
<!-- 底部操作栏 -->
<view class="action-bar-wrapper safe-area-bottom">
<view class="action-bar">
<button class="action-btn share-btn" open-type="share">
<u-icon name="weixin-fill" color="#ccc" size="20"></u-icon>
<text>分享</text>
</button>
<view class="btn-divider"></view>
<button class="action-btn add-btn" @tap="showModal">
<text>加入清单</text>
</button>
</view>
</view>
</view>
</template>
<script>
import { getProductDetailApi } from "@/api/page/product";
import { getCartListApi, toCartApi } from "@/api/page/cart";
import {getCache} from "@/utils/cache";
import { urlSafeBase64Encode } from "@/utils/utils";
export default {
data() {
return {
id: 0,
p_user_id: 0,
listModal: false,
listOption: [],
product: {
title: '',
cover: '',
price_sheet: [],
alias: '',
carousel: []
}
};
},
onLoad(options) {
this.id = options.id;
this.p_user_id = options.p_user_id;
this.initAlias();
this.getProductDetail();
},
onShareAppMessage() {
const userInfo = getCache('user_info')
return {
title: this.product.title,
path: `/pages/product/detail?id=${this.id}&p_user_id=${userInfo.id}`,
imageUrl: this.product.cover || this.product.carousel[0] || ''
};
},
methods: {
previewImages(url) {
let w = this.product.title;
w = urlSafeBase64Encode(w);
console.log(`${url}?watermark/2/text/${w}/fontsize/800`);
uni.previewImage({
current: 0,
urls: [`${url}?watermark/2/text/${w}/fontsize/800`],
});
},
navigateBack() {
uni.navigateBack();
},
showModal() {
this.getListOption();
this.listModal = true;
},
addToCart(id) {
toCartApi(id, this.id).then(() => {
this.listModal = false;
uni.showToast({
title: '已添加到清单',
icon: 'success'
});
});
},
getProductDetail() {
getProductDetailApi(this.id, this.p_user_id).then((res) => {
this.product = {
...res,
cover: res.cover || res.carousel[0] || ''
};
this.initAlias();
});
},
initAlias() {
if(this.product.alias) {
this.product.alias = this.product.alias.replace(/\n/g, '<br/>');
}
},
getListOption() {
getCartListApi().then((res) => {
this.listOption = res;
});
}
}
};
</script>
<style lang="scss">
.container {
min-height: 100vh;
background-color: #F7F8FA;
}
@keyframes slideUp {
from { transform: translateY(60rpx); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@keyframes scaleIn {
from { transform: scale(0); }
to { transform: scale(1); }
}
.animate-slide-up { animation: slideUp 0.6s cubic-bezier(0.2, 0.8, 0.2, 1); }
.animate-scale { animation: scaleIn 0.4s ease-out backwards; }
/* Swiper */
.swiper-container {
position: relative;
background-color: #fff;
}
.product-swiper {
height: 750rpx;
}
.product-image {
width: 100%;
height: 100%;
}
/* 始终悬浮的分享按钮 */
.share-fixed-btn {
position: fixed;
top: 40rpx;
right: 30rpx;
width: 72rpx;
height: 72rpx;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(8px);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.1);
z-index: 999;
}
/* Info Card */
.info-card {
background: #fff;
border-radius: 32rpx 32rpx 0 0;
/* 关键修改移除负边距改为0让内容自然连接 */
margin-top: 0;
position: relative;
z-index: 2;
padding: 40rpx 30rpx;
min-height: 500rpx;
/* 增加一个小阴影让层级更明显 */
box-shadow: 0 -4rpx 12rpx rgba(0,0,0,0.02);
}
.title-section {
margin-bottom: 40rpx;
}
.product-code {
font-size: 40rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
display: block;
}
.product-alias {
font-size: 28rpx;
color: #666;
line-height: 1.6;
}
/* Specs */
.section-title {
font-size: 32rpx;
font-weight: 700;
color: #333;
margin-bottom: 24rpx;
position: relative;
padding-left: 16rpx;
}
.section-title::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 6rpx;
height: 28rpx;
background: #B4854D;
border-radius: 4rpx;
}
.specs-table {
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
border: 2rpx solid #f0f0f0;
}
.specs-header, .specs-row {
display: grid;
padding: 20rpx;
}
.cols-1 { grid-template-columns: 1fr; }
.cols-2 { grid-template-columns: 1fr 1fr; }
.specs-header {
background-color: #F7F8FA;
font-size: 26rpx;
color: #666;
font-weight: 600;
}
.specs-row {
border-top: 1rpx solid #f0f0f0;
}
.cell-spec {
padding-right: 10rpx;
}
.spec-name {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.spec-dim {
font-size: 24rpx;
color: #999;
margin-top: 4rpx;
}
.price-tag {
font-size: 26rpx;
color: #B4854D;
font-weight: 500;
margin-bottom: 4rpx;
}
/* Details */
.detail-section {
margin-top: 50rpx;
}
.detail-image {
width: 100%;
margin-bottom: 10rpx;
border-radius: 8rpx;
}
/* Safe Padding Bottom */
.safe-padding-bottom {
height: 160rpx;
width: 100%;
}
/* Action Bar */
.action-bar-wrapper {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.05);
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
z-index: 100;
}
.action-bar {
display: flex;
align-items: center;
justify-content: space-between;
height: 90rpx;
background: #333;
border-radius: 45rpx;
padding: 0 10rpx;
overflow: hidden;
}
.action-btn {
flex: 1;
background: transparent;
color: #fff;
border-radius: 0;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
gap: 10rpx;
}
.action-btn::after { border: none; }
.add-btn {
background: #B4854D;
color: #fff;
height: 80%;
border-radius: 40rpx;
margin-right: 10rpx;
font-weight: 600;
}
.share-btn {
color: #ccc;
}
/* Popup */
.popup-content {
padding: 30rpx;
}
.list-options {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.option-item {
background: #fff;
padding: 24rpx;
border-radius: 16rpx;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.02);
}
.option-name {
font-size: 30rpx;
color: #333;
font-weight: 500;
display: block;
}
.option-id {
font-size: 22rpx;
color: #999;
}
.add-icon-btn {
width: 50rpx;
height: 50rpx;
background: #333;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
</style>

559
pages/product/product.vue Normal file
View File

@@ -0,0 +1,559 @@
<template>
<view class="container">
<!-- Search Bar (独立按钮版) -->
<view class="header-section">
<view class="search-bar-row">
<view class="search-input-box">
<up-search
type="text"
placeholder="搜索产品型号"
placeholder-class="search-placeholder"
v-model="title"
:showAction="false"
:animation="false"
bgColor="#f5f5f5"
shape="round"
@search="handleSearch"
/>
</view>
<view class="custom-search-btn" @tap="handleSearch" hover-class="btn-hover">
搜索
</view>
</view>
<!-- Category Tabs (Sticky) -->
<view class="tabs-wrapper">
<scroll-view
class="category-tabs"
scroll-x
show-scrollbar="false"
:scroll-into-view="'tab-' + currentTab"
scroll-with-animation
>
<view
v-for="(tab, index) in tabs"
:key="index"
:id="'tab-' + index"
:class="['tab-item', { active: currentTab === index }]"
@tap="switchTab(index, tab.id)"
>
<text class="tab-text">{{ tab.name }}</text>
<view class="active-dot" v-if="currentTab === index"></view>
</view>
</scroll-view>
</view>
</view>
<!-- Content Area -->
<view class="content-area">
<!-- Loading State (Skeleton Screen) -->
<view class="loading-container" v-if="loading && products.length === 0">
<!-- CSS 骨架屏结构 -->
<view class="waterfall-container">
<view class="waterfall-column">
<view class="skeleton-card" v-for="i in 3" :key="'l'+i">
<view class="skeleton-img pulse"></view>
<view class="skeleton-text pulse"></view>
</view>
</view>
<view class="waterfall-column">
<view class="skeleton-card" v-for="i in 3" :key="'r'+i">
<view class="skeleton-img pulse" style="height: 300rpx"></view>
<view class="skeleton-text pulse"></view>
</view>
</view>
</view>
</view>
<!-- Empty State -->
<view class="empty-container" v-else-if="products.length === 0">
<image src="/static/images/empty.png" mode="aspectFit" class="empty-image animate-float" />
<text class="empty-text">暂无相关产品</text>
</view>
<!-- Waterfall Layout -->
<view class="waterfall-container" v-else>
<view class="waterfall-column">
<view
v-for="(product, index) in leftProducts"
:key="product.id"
class="product-item animate-card-enter"
:style="{ animationDelay: index * 0.1 + 's' }"
@tap="navigateToDetail(product.id)"
>
<view class="img-box">
<image
:src="product.cover"
mode="widthFix"
class="product-image"
@load="imageLoaded(index * 2)"
/>
</view>
<view class="info-box">
<text class="product-code">{{ product.title }}</text>
</view>
</view>
</view>
<view class="waterfall-column">
<view
v-for="(product, index) in rightProducts"
:key="product.id"
class="product-item animate-card-enter"
:style="{ animationDelay: (index * 0.1 + 0.05) + 's' }"
@tap="navigateToDetail(product.id)"
>
<view class="img-box">
<image
:src="product.cover"
mode="widthFix"
class="product-image"
@load="imageLoaded(index * 2 + 1)"
/>
</view>
<view class="info-box">
<text class="product-code">{{ product.title }}</text>
</view>
</view>
</view>
</view>
<!-- Footer State -->
<view class="footer-state">
<view class="loading-more" v-if="loadingMore">
<view class="loading-spinner"></view>
</view>
<view class="no-more-data" v-if="noMoreData && products.length > 0">
<text class="divider-line"></text>
<text>THE END</text>
<text class="divider-line"></text>
</view>
</view>
</view>
</view>
</template>
<script>
import { getCategoryListByPidApi, getProductListApi } from "@/api/page/product"
export default {
data() {
return {
page: 1,
pageSize: 10,
pid: 0,
currentTab: 0,
categoryId: 0,
title: "",
tabs: [],
products: [],
loading: false,
loadingMore: false,
noMoreData: false,
totalPages: 1,
}
},
computed: {
// 将产品分为左右两列,实现瀑布流布局
leftProducts() {
return this.products.filter((_, index) => index % 2 === 0)
},
rightProducts() {
return this.products.filter((_, index) => index % 2 === 1)
},
},
onLoad(options) {
if (options && options.pid) {
this.pid = options.pid
this.categoryId = options.pid
this.getCategoryList()
} else {
uni.showToast({
title: "参数错误",
icon: "none",
})
}
},
onPullDownRefresh() {
this.refreshProductList()
},
onReachBottom() {
this.loadMoreProducts()
},
mounted() {
this.getProductList()
},
methods: {
navigateBack() {
uni.navigateBack()
},
switchTab(index, id) {
this.currentTab = index
this.categoryId = id
this.page = 1
this.products = []
this.noMoreData = false
this.getProductList()
},
navigateToDetail(id) {
uni.navigateTo({
url: `/pages/product/detail?id=${id}`,
})
},
getCategoryList() {
getCategoryListByPidApi(this.pid).then((res) => {
res.unshift({
id: 0,
name: "全部",
})
this.tabs = res
})
},
handleSearch() {
// 这里的搜索逻辑是正确的:重置分页和数据,重新请求
this.page = 1
this.products = []
this.noMoreData = false
this.getProductList()
},
refreshProductList() {
this.page = 1
this.products = []
this.noMoreData = false
this.getProductList()
.then(() => {
uni.stopPullDownRefresh()
uni.showToast({
title: "刷新成功",
icon: "success",
duration: 1500,
})
})
.catch(() => {
uni.stopPullDownRefresh()
uni.showToast({
title: "刷新失败",
icon: "none",
duration: 1500,
})
})
},
loadMoreProducts() {
if (this.loadingMore || this.noMoreData) return
this.page++
this.loadingMore = true
getProductListApi({
page: this.page,
size: this.pageSize,
title: this.title,
category_id: this.categoryId,
})
.then((res) => {
if (res.data && res.data.length > 0) {
this.products = [...this.products, ...res.data]
if (res.data.length < this.pageSize || this.page >= this.totalPages) {
this.noMoreData = true
}
} else {
this.noMoreData = true
}
this.loadingMore = false
})
.catch(() => {
this.page--
this.loadingMore = false
uni.showToast({
title: "加载失败",
icon: "none",
})
})
},
getProductList() {
this.loading = true
return new Promise((resolve, reject) => {
getProductListApi({
page: this.page,
size: this.pageSize,
title: this.title,
category_id: this.categoryId,
})
.then((res) => {
if (this.page === 1) {
this.products = res.data || []
} else {
this.products = [...this.products, ...(res.data || [])]
}
if (!res.data || res.data.length < this.pageSize) {
this.noMoreData = true
}
if (res.last_page) {
this.totalPages = res.last_page
}
this.loading = false
resolve(res)
})
.catch((err) => {
this.loading = false
uni.showToast({
title: "加载失败",
icon: "none",
})
reject(err)
})
})
},
imageLoaded(index) {
},
},
}
</script>
<style lang="scss">
.container {
min-height: 100vh;
background-color: #F7F8FA;
display: flex;
flex-direction: column;
}
.header-section {
background: #fff;
position: sticky;
top: 0;
z-index: 10;
box-shadow: 0 4rpx 10rpx rgba(0,0,0,0.02);
}
/* 独立的搜索行样式 */
.search-bar-row {
display: flex;
align-items: center;
padding: 20rpx 30rpx;
gap: 20rpx;
}
.search-input-box {
flex: 1;
}
.custom-search-btn {
padding: 12rpx 30rpx;
background: #B4854D;
color: #fff;
font-size: 28rpx;
border-radius: 40rpx;
font-weight: 500;
box-shadow: 0 4rpx 10rpx rgba(180, 133, 77, 0.3);
transition: opacity 0.2s;
}
.btn-hover {
opacity: 0.8;
}
.tabs-wrapper {
border-bottom: 1rpx solid #f0f0f0;
}
.category-tabs {
white-space: nowrap;
padding: 0 10rpx;
height: 90rpx;
}
.tab-item {
display: inline-block;
padding: 0 30rpx;
height: 90rpx;
line-height: 90rpx;
position: relative;
transition: all 0.3s ease;
}
.tab-text {
font-size: 28rpx;
color: #666;
font-weight: 400;
transition: all 0.3s;
}
.tab-item.active .tab-text {
color: #B4854D;
font-size: 30rpx;
font-weight: 600;
}
.active-dot {
position: absolute;
bottom: 16rpx;
left: 50%;
transform: translateX(-50%);
width: 8rpx;
height: 8rpx;
background-color: #B4854D;
border-radius: 50%;
box-shadow: 0 2rpx 4rpx rgba(180, 133, 77, 0.4);
}
/* 瀑布流布局优化 */
.waterfall-container {
display: flex;
padding: 24rpx;
align-items: flex-start;
}
.waterfall-column {
flex: 1;
display: flex;
flex-direction: column;
gap: 24rpx;
}
.waterfall-column:first-child {
padding-right: 12rpx;
}
.waterfall-column:last-child {
padding-left: 12rpx;
}
.product-item {
background-color: #ffffff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.03);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.product-item:active {
transform: scale(0.98);
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.02);
}
.product-image {
width: 100%;
height: auto;
display: block;
}
.info-box {
padding: 24rpx 20rpx;
}
.product-code {
display: block;
font-size: 28rpx;
color: #333333;
line-height: 1.4;
font-weight: 500;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* 动画 Keyframes */
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(40rpx); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes pulse {
0% { opacity: 0.6; background: #eee; }
50% { opacity: 1; background: #ddd; }
100% { opacity: 0.6; background: #eee; }
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes float {
0% { transform: translateY(0); }
50% { transform: translateY(-10rpx); }
100% { transform: translateY(0); }
}
.animate-card-enter {
animation: fadeInUp 0.6s cubic-bezier(0.2, 0.8, 0.2, 1) backwards;
}
.animate-float {
animation: float 3s ease-in-out infinite;
}
/* 骨架屏样式 */
.skeleton-card {
background: #fff;
border-radius: 16rpx;
overflow: hidden;
margin-bottom: 24rpx;
}
.skeleton-img {
width: 100%;
height: 240rpx;
background: #eee;
}
.skeleton-text {
height: 30rpx;
margin: 20rpx;
width: 60%;
background: #eee;
border-radius: 8rpx;
}
.pulse {
animation: pulse 1.5s infinite ease-in-out;
}
/* 底部状态 */
.loading-more {
display: flex;
justify-content: center;
padding: 30rpx 0;
}
.loading-spinner {
width: 30rpx;
height: 30rpx;
border: 4rpx solid #ddd;
border-top-color: #B4854D;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.no-more-data {
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx 0;
color: #ccc;
font-size: 22rpx;
letter-spacing: 2rpx;
}
.divider-line {
width: 40rpx;
height: 2rpx;
background: #eee;
margin: 0 16rpx;
}
/* 空状态 */
.empty-container {
padding-top: 200rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.empty-image {
width: 240rpx;
height: 240rpx;
opacity: 0.8;
}
</style>

399
pages/search/search.vue Normal file
View File

@@ -0,0 +1,399 @@
<template>
<view class="container">
<!-- Search Bar (独立按钮版) -->
<view class="search-section">
<view class="search-bar-row">
<view class="search-input-box">
<up-search
type="text"
placeholder="搜索产品型号"
placeholder-class="search-placeholder"
v-model="title"
:showAction="false"
:animation="false"
bgColor="#f5f5f5"
shape="round"
@search="handleSearch"
/>
</view>
<view class="custom-search-btn" @tap="handleSearch" hover-class="btn-hover">
搜索
</view>
</view>
</view>
<view class="content-body">
<!-- Loading State (Skeleton) -->
<view class="loading-container" v-if="loading && products.length === 0">
<view class="waterfall-container">
<view class="waterfall-column">
<view class="skeleton-card animate-pulse" v-for="i in 3" :key="'l'+i">
<view class="skeleton-img"></view>
<view class="skeleton-txt"></view>
</view>
</view>
<view class="waterfall-column">
<view class="skeleton-card animate-pulse" v-for="i in 3" :key="'r'+i">
<view class="skeleton-img" style="height: 300rpx"></view>
<view class="skeleton-txt"></view>
</view>
</view>
</view>
</view>
<!-- Empty State -->
<view class="empty-container animate-fade-in" v-else-if="products.length === 0">
<image src="/static/images/empty.png" mode="aspectFit" class="empty-image" />
<text class="empty-text">未找到相关产品</text>
</view>
<!-- Waterfall Layout -->
<view class="waterfall-container" v-else>
<view class="waterfall-column">
<view
v-for="(product, index) in leftProducts"
:key="product.id"
class="product-item animate-card-enter"
:style="{ animationDelay: index * 0.05 + 's' }"
@tap="navigateToDetail(product.id)"
>
<image
:src="product.cover"
mode="widthFix"
class="product-image"
@load="imageLoaded(index * 2)"
/>
<view class="product-info">
<text class="product-code">{{ product.title }}</text>
</view>
</view>
</view>
<view class="waterfall-column">
<view
v-for="(product, index) in rightProducts"
:key="product.id"
class="product-item animate-card-enter"
:style="{ animationDelay: (index * 0.05 + 0.05) + 's' }"
@tap="navigateToDetail(product.id)"
>
<image
:src="product.cover"
mode="widthFix"
class="product-image"
@load="imageLoaded(index * 2 + 1)"
/>
<view class="product-info">
<text class="product-code">{{ product.title }}</text>
</view>
</view>
</view>
</view>
<!-- Loading Footer -->
<view class="loading-more" v-if="loadingMore">
<view class="spinner"></view>
</view>
<view class="no-more-data" v-if="noMoreData && products.length > 0">
<text class="line"></text>
<text>THE END</text>
<text class="line"></text>
</view>
</view>
</view>
</template>
<script>
import { getProductListApi } from "@/api/page/product"
export default {
data() {
return {
page: 1,
pageSize: 10,
title: "",
products: [],
loading: false,
loadingMore: false,
noMoreData: false,
totalPages: 1,
}
},
computed: {
leftProducts() {
return this.products.filter((_, index) => index % 2 === 0)
},
rightProducts() {
return this.products.filter((_, index) => index % 2 === 1)
},
},
onLoad(options) {
if (options && options.title) {
this.title = options.title
}
this.getProductList()
},
onPullDownRefresh() {
this.refreshProductList()
},
onReachBottom() {
this.loadMoreProducts()
},
methods: {
navigateToDetail(id) {
uni.navigateTo({
url: `/pages/product/detail?id=${id}`,
})
},
handleSearch() {
this.page = 1
this.products = []
this.noMoreData = false
this.getProductList()
},
refreshProductList() {
this.page = 1
this.products = []
this.noMoreData = false
this.getProductList()
.then(() => {
uni.stopPullDownRefresh()
uni.showToast({
title: "刷新成功",
icon: "success",
duration: 1500,
})
})
.catch(() => {
uni.stopPullDownRefresh()
uni.showToast({
title: "刷新失败",
icon: "none",
duration: 1500,
})
})
},
loadMoreProducts() {
if (this.loadingMore || this.noMoreData) return
this.page++
this.loadingMore = true
getProductListApi({
page: this.page,
size: this.pageSize,
title: this.title,
})
.then((res) => {
if (res.data && res.data.length > 0) {
this.products = [...this.products, ...res.data]
if (res.data.length < this.pageSize || this.page >= this.totalPages) {
this.noMoreData = true
}
} else {
this.noMoreData = true
}
this.loadingMore = false
})
.catch(() => {
this.page--
this.loadingMore = false
uni.showToast({
title: "加载失败",
icon: "none",
})
})
},
getProductList() {
this.loading = true
return new Promise((resolve, reject) => {
getProductListApi({
page: this.page,
size: this.pageSize,
title: this.title,
})
.then((res) => {
if (this.page === 1) {
this.products = res.data || []
} else {
this.products = [...this.products, ...(res.data || [])]
}
if (!res.data || res.data.length < this.pageSize) {
this.noMoreData = true
}
if (res.last_page) {
this.totalPages = res.last_page
}
this.loading = false
uni.hideLoading()
resolve(res)
})
.catch((err) => {
this.loading = false
uni.hideLoading()
uni.showToast({
title: "加载失败",
icon: "none",
})
reject(err)
})
})
},
imageLoaded(index) {
},
},
}
</script>
<style lang="scss">
.container {
min-height: 100vh;
background-color: #F7F8FA;
display: flex;
flex-direction: column;
}
@keyframes pulse {
0% { opacity: 0.6; background: #e0e0e0; }
50% { opacity: 1; background: #d0d0d0; }
100% { opacity: 0.6; background: #e0e0e0; }
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(40rpx); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes spin { to { transform: rotate(360deg); } }
.animate-pulse { animation: pulse 1.5s infinite ease-in-out; }
.animate-card-enter { animation: fadeInUp 0.6s ease-out backwards; }
.animate-fade-in { animation: fadeInUp 0.5s ease-out; }
.search-section {
background: #fff;
padding: 20rpx 30rpx;
position: sticky;
top: 0;
z-index: 10;
box-shadow: 0 4rpx 10rpx rgba(0,0,0,0.02);
}
.search-bar-row {
display: flex;
align-items: center;
gap: 20rpx;
}
.search-input-box {
flex: 1;
}
.custom-search-btn {
padding: 12rpx 30rpx;
background: #B4854D;
color: #fff;
font-size: 28rpx;
border-radius: 40rpx;
font-weight: 500;
box-shadow: 0 4rpx 10rpx rgba(180, 133, 77, 0.3);
transition: opacity 0.2s;
}
.btn-hover {
opacity: 0.8;
}
.content-body {
flex: 1;
}
/* 瀑布流 */
.waterfall-container {
display: flex;
padding: 24rpx;
align-items: flex-start;
}
.waterfall-column {
flex: 1;
display: flex;
flex-direction: column;
gap: 24rpx;
}
.waterfall-column:first-child { padding-right: 12rpx; }
.waterfall-column:last-child { padding-left: 12rpx; }
.product-item {
background: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 8rpx 20rpx rgba(0,0,0,0.03);
}
.product-image {
width: 100%;
height: auto;
display: block;
}
.product-info {
padding: 20rpx;
}
.product-code {
font-size: 28rpx;
color: #333;
line-height: 1.4;
font-weight: 500;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Skeleton */
.skeleton-card {
background: #fff;
border-radius: 16rpx;
overflow: hidden;
margin-bottom: 24rpx;
}
.skeleton-img { width: 100%; height: 240rpx; background: #eee; }
.skeleton-txt { height: 30rpx; width: 70%; background: #eee; margin: 20rpx; border-radius: 8rpx; }
/* Empty */
.empty-container {
padding-top: 150rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.empty-image { width: 240rpx; height: 240rpx; opacity: 0.8; margin-bottom: 20rpx; }
.empty-text { color: #999; font-size: 28rpx; }
/* Loading Footer */
.loading-more {
display: flex;
justify-content: center;
padding: 30rpx 0;
}
.spinner {
width: 36rpx;
height: 36rpx;
border: 4rpx solid #e0e0e0;
border-top-color: #B4854D;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.no-more-data {
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx 0;
color: #ccc;
font-size: 22rpx;
}
.line { width: 40rpx; height: 1px; background: #eee; margin: 0 10rpx; }
</style>

76
pnpm-lock.yaml generated Normal file
View File

@@ -0,0 +1,76 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
clipboard:
specifier: ^2.0.11
version: 2.0.11
dayjs:
specifier: ^1.11.13
version: 1.11.13
js-base64:
specifier: ^3.7.7
version: 3.7.8
uview-plus:
specifier: ^3.3.36
version: 3.3.36
packages:
clipboard@2.0.11:
resolution: {integrity: sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==, tarball: https://registry.npmmirror.com/clipboard/-/clipboard-2.0.11.tgz}
dayjs@1.11.13:
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==, tarball: https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz}
delegate@3.2.0:
resolution: {integrity: sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==, tarball: https://registry.npmmirror.com/delegate/-/delegate-3.2.0.tgz}
good-listener@1.2.2:
resolution: {integrity: sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==, tarball: https://registry.npmmirror.com/good-listener/-/good-listener-1.2.2.tgz}
js-base64@3.7.8:
resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==}
select@1.1.2:
resolution: {integrity: sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==, tarball: https://registry.npmmirror.com/select/-/select-1.1.2.tgz}
tiny-emitter@2.1.0:
resolution: {integrity: sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==, tarball: https://registry.npmmirror.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz}
uview-plus@3.3.36:
resolution: {integrity: sha512-yiIIt3OkCDkBzflrr6by8qvYibvCzsFZ/Mn+Fdx9TDaTQAltWFBj7UNVN/wOeb3lou2T5S1QFy2rxw3OcAYi5g==, tarball: https://registry.npmmirror.com/uview-plus/-/uview-plus-3.3.36.tgz}
engines: {HBuilderX: ^3.1.0}
snapshots:
clipboard@2.0.11:
dependencies:
good-listener: 1.2.2
select: 1.1.2
tiny-emitter: 2.1.0
dayjs@1.11.13: {}
delegate@3.2.0: {}
good-listener@1.2.2:
dependencies:
delegate: 3.2.0
js-base64@3.7.8: {}
select@1.1.2: {}
tiny-emitter@2.1.0: {}
uview-plus@3.3.36:
dependencies:
clipboard: 2.0.11
dayjs: 1.11.13

BIN
static/icon/appointment.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
static/icon/await.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
static/icon/doing.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
static/icon/order.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
static/icon/served.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
static/icon/wallet.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
static/images/empty.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
static/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

1
static/loading.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="150" style="display:block;margin:0 auto"><defs><radialGradient id="a" cx="50%" cy="50%" r="50%" fx="50%" fy="50%"><stop offset="0%" style="stop-color:#fff;stop-opacity:1"/><stop offset="100%" style="stop-color:#ffb65c;stop-opacity:1"/></radialGradient><radialGradient id="c" cx="50%" cy="50%" r="50%" fx="50%" fy="50%"><stop offset="0%" style="stop-color:#fff;stop-opacity:1"/><stop offset="30%" style="stop-color:#fff;stop-opacity:1"/><stop offset="40%" style="stop-color:#ffffc8;stop-opacity:.8"/><stop offset="50%" style="stop-color:#ffff96;stop-opacity:.6"/><stop offset="60%" style="stop-color:#ffff64;stop-opacity:.4"/><stop offset="70%" style="stop-color:#ffff32;stop-opacity:.2"/><stop offset="80%" style="stop-color:#ffe600;stop-opacity:.1"/><stop offset="90%" style="stop-color:#ffb65c;stop-opacity:.05"/><stop offset="100%" style="stop-color:#ffb65c;stop-opacity:0"/></radialGradient><filter id="b"><feTurbulence baseFrequency=".1" numOctaves="2" result="turbulence" type="fractalNoise"/><feDisplacementMap in="SourceGraphic" in2="turbulence" scale="10"/></filter></defs><rect width="160" height="110" x="20" y="20" fill="url(#a)" rx="20" ry="20"/><circle cx="100" cy="60" r="10" fill="url(#a)"/><rect width="160" height="110" x="20" y="20" fill="url(#a)" filter="url(#b)" rx="20" ry="20"/><circle cx="30" cy="40" r="10" fill="url(#c)"/><circle cx="40" cy="90" r="15" fill="url(#c)"/><circle cx="110" cy="40" r="12" fill="url(#c)"/><circle cx="100" cy="60" r="10" fill="url(#c)"/><circle cx="160" cy="80" r="15" fill="url(#c)"/><g opacity=".2" transform="matrix(1.2 0 0 1.2 60 20)"><path fill="#f80" d="M4.5 2.2c-3.5 2.2-3.4 25.5.8 33l26.1-16c-4-7.1-23.6-19-26.9-17"/><path fill="#f7a4a4" d="M9.5 11.3c-1.5.9-2.2 16.2.4 21l16.7-10.2c-2.4-4.6-15.7-11.6-17.1-10.8"/><path fill="#f80" d="M59.5 2.2c3.5 2.2 3.4 25.5-.7 33l-26.1-16c3.9-7.1 23.5-19 26.8-17"/><path fill="#f7a4a4" d="M54.5 11.3c1.5.9 2.2 16.2-.4 21L37.3 22.1c2.5-4.6 15.8-11.6 17.2-10.8"/><path fill="#f80" d="M31.8 13.1C4.7 13.1 2.2 32.5 2.2 43.5 2.2 48 15.4 62 31.8 62c16.4 0 29.6-14 29.6-18.5 0-11-2.5-30.4-29.6-30.4"/><path fill="#fff" d="M24.2 38.7s-3.1 4.8-8.8 3.3c-5.7-1.5-6-7.2-6-7.2s3.1-4.8 8.8-3.3c5.8 1.5 6 7.2 6 7.2"/><path fill="#fff" d="M23.6 36.2s-2.7 3-6.5 3c-4.1 0-6.5-5.9-6.5-5.9s2.7-3.1 7.6-1.9c4.5 1.2 5.4 4.8 5.4 4.8"/><path fill="#f80" d="M19.4 36.1c0 6.6-3.2 6.6-3.2 0 .1-6.6 3.2-6.6 3.2 0"/><path fill="#fff" d="M39.4 38.7s3.1 4.8 8.8 3.3c5.7-1.5 6-7.2 6-7.2s-3.1-4.8-8.8-3.3c-5.8 1.5-6 7.2-6 7.2"/><path fill="#fff" d="M39.9 36.2s2.7 3 6.5 3c4.1 0 6.5-5.9 6.5-5.9s-2.7-3.1-7.6-1.9c-4.5 1.2-5.4 4.8-5.4 4.8"/><path fill="#f80" d="M44.1 36.1c0 6.6 3.2 6.6 3.2 0s-3.2-6.6-3.2 0"/><path fill="#fff" d="M40.4 44c-2.6-2-5.4-8.7-8.6-8.7S25.7 42 23.1 44c-4.1 3.2-15 6.8-15 6.8S19.7 61 31.7 61s23.6-10.2 23.6-10.2c.1 0-10.8-3.6-14.9-6.8"/><ellipse cx="31.8" cy="54.5" fill="#ff94a4" rx="1.7" ry="2.5"/><g fill="#f80"><path d="M40.2 53.1c-1 .6-2.1.8-3.1.8-1.1-.1-2.1-.4-2.9-1.1-.8-.6-1.4-1.5-1.6-2.6l-.8-4.5-.8 4.5c-.2 1-.8 1.9-1.6 2.6-.8.7-1.9 1-2.9 1.1-1.1 0-2.2-.2-3.1-.8-1-.6-1.8-1.5-2.2-2.7.1 1.3.7 2.5 1.7 3.4 1 .9 2.3 1.4 3.5 1.4 1.4.1 2.7-.3 3.8-1.1.6-.4 1.1-1 1.5-1.6.4.6.9 1.2 1.5 1.6 1.1.8 2.5 1.2 3.8 1.1 1.3-.1 2.6-.6 3.6-1.4 1-.9 1.7-2.1 1.7-3.4-.3 1.1-1.1 2.1-2.1 2.7"/><path d="M35.8 44.8c-.8-1-3.3-1.1-4-1.1-.7 0-3.2.1-4 1.1-.6.7-.1 2.5 1.4 4 1 1 1.9 1.3 2.6 1.3.7 0 1.7-.3 2.6-1.3 1.5-1.6 2-3.3 1.4-4"/></g></g><text x="50" y="120" font-size="35" font-family="楷体" font-weight="bold" fill="#ff8800">奶酪云</text><g fill="orange" transform="translate(70 75)"><circle id="d" cx="0" cy="0" r="5"/><circle id="e" cx="20" cy="0" r="5"/><circle id="f" cx="40" cy="0" r="5"/><circle id="g" cx="60" cy="0" r="5"/></g><animate xlink:href="#d" attributeName="cy" begin="0s" dur="0.75s" from="0" keyTimes="0;0.5;1" repeatCount="indefinite" to="-10" values="0;-10;0"/><animate xlink:href="#e" attributeName="cy" begin="0.1s" dur="0.75s" from="0" keyTimes="0;0.5;1" repeatCount="indefinite" to="-10" values="0;-10;0"/><animate xlink:href="#f" attributeName="cy" begin="0.2s" dur="0.75s" from="0" keyTimes="0;0.5;1" repeatCount="indefinite" to="-10" values="0;-10;0"/><animate xlink:href="#g" attributeName="cy" begin="0.3s" dur="1s" from="0" keyTimes="0;0.5;1" repeatCount="indefinite" to="-10" values="0;-10;0"/></svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
static/tabbar/activity.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
static/tabbar/classify.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
static/tabbar/home-fill.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
static/tabbar/home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
static/tabbar/mine-fill.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
static/tabbar/mine.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
static/tabbar/v2/home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
static/tabbar/v2/mine.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

10
uni.promisify.adaptor.js Normal file
View File

@@ -0,0 +1,10 @@
uni.addInterceptor({
returnValue (res) {
if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) {
return res;
}
return new Promise((resolve, reject) => {
res.then((res) => res[0] ? reject(res[0]) : resolve(res[1]));
});
},
});

79
uni.scss Normal file
View File

@@ -0,0 +1,79 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* uni.scss */
@import 'uview-plus/theme.scss';
/* 颜色变量 */
/* 行为相关颜色 */
$uni-color-primary: #007aff;
$uni-color-success: #4cd964;
$uni-color-warning: #f0ad4e;
$uni-color-error: #dd524d;
/* 文字基本颜色 */
$uni-text-color:#333;//基本色
$uni-text-color-inverse:#fff;//反色
$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
$uni-text-color-placeholder: #808080;
$uni-text-color-disable:#c0c0c0;
/* 背景颜色 */
$uni-bg-color:#ffffff;
$uni-bg-color-grey:#f8f8f8;
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
/* 边框颜色 */
$uni-border-color:#c8c7cc;
/* 尺寸变量 */
/* 文字尺寸 */
$uni-font-size-sm:12px;
$uni-font-size-base:14px;
$uni-font-size-lg:16px;
/* 图片尺寸 */
$uni-img-size-sm:20px;
$uni-img-size-base:26px;
$uni-img-size-lg:40px;
/* Border Radius */
$uni-border-radius-sm: 2px;
$uni-border-radius-base: 3px;
$uni-border-radius-lg: 6px;
$uni-border-radius-circle: 50%;
/* 水平间距 */
$uni-spacing-row-sm: 5px;
$uni-spacing-row-base: 10px;
$uni-spacing-row-lg: 15px;
/* 垂直间距 */
$uni-spacing-col-sm: 4px;
$uni-spacing-col-base: 8px;
$uni-spacing-col-lg: 12px;
/* 透明度 */
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
/* 文章场景相关 */
$uni-color-title: #2C405A; // 文章标题颜色
$uni-font-size-title:20px;
$uni-color-subtitle: #555555; // 二级标题颜色
$uni-font-size-subtitle:26px;
$uni-color-paragraph: #3F536E; // 文章段落颜色
$uni-font-size-paragraph:15px;

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 https://uiadmin.net/uview-plus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,74 @@
<p align="center">
<img alt="logo" src="https://uiadmin.net/uview-plus/common/logo.png" width="120" height="120" style="margin-bottom: 10px;">
</p>
<h3 align="center" style="margin: 30px 0 30px;font-weight: bold;font-size:40px;">uview-plus 3.0</h3>
<h3 align="center">多平台快速开发的UI框架</h3>
[![stars](https://img.shields.io/github/stars/ijry/uview-plus?style=flat-square&logo=GitHub)](https://github.com/ijry/uview-plus)
[![forks](https://img.shields.io/github/forks/ijry/uview-plus?style=flat-square&logo=GitHub)](https://github.com/ijry/uview-plus)
[![issues](https://img.shields.io/github/issues/ijry/uview-plus?style=flat-square&logo=GitHub)](https://github.com/ijry/uview-plus/issues)
[![release](https://img.shields.io/github/v/release/ijry/uview-plus?style=flat-square)](https://gitee.com/jry/uview-plus/releases)
[![license](https://img.shields.io/github/license/ijry/uview-plus?style=flat-square)](https://en.wikipedia.org/wiki/MIT_License)
## 说明
uview-plus是uni-app全面兼容vue3/nvue/鸿蒙/uni-app-x的uni-app生态框架全面的组件和便捷的工具会让您信手拈来如鱼得水。uview-plus是基于uView2.x移植的支持vue3的版本感谢uView。
## 可视化设计
uview-plus现已推出免费可视化设计可以方便的进行页面可视化设计导出源码即可使用。极大提高前端页面开发效率如产品经理设计师直接使用更可作为高保真高可用原型制作工具让设计稿即代码无需传统的设计稿开发还原步骤。
<img src="https://s3.bmp.ovh/imgs/2024/11/24/fd58d00071e6e5df.png" width="900" height="auto" >
<img src="https://s3.bmp.ovh/imgs/2024/11/24/8e85a519fe627fb1.png" width="900" height="auto" >
## 文档
[官方文档https://uview-plus.jiangruyi.com](https://uview-plus.jiangruyi.com)
[备用文档https://uiadmin.net/uview-plus](https://uiadmin.net/uview-plus)
## 预览
您可以通过**微信**扫码,查看最佳的演示效果。
<br>
<br>
<img src="https://uview-plus.jiangruyi.com/common/h5_qrcode.png" width="220" height="220" >
## 链接
- [官方文档](https://uview-plus.jiangruyi.com)
- [更新日志](https://uview-plus.jiangruyi.com/components/changelog.html)
- [升级指南](https://uview-plus.jiangruyi.com/components/changeGuide.html)
- [关于我们](https://uview-plus.jiangruyi.com/cooperation/about.html)
## 交流反馈
欢迎加入我们的QQ群交流反馈[点此跳转](https://uview-plus.jiangruyi.com/components/addQQGroup.html)
## 关于PR
> 我们非常乐意接受各位的优质PR但在此之前我希望您了解uview-plus是一个需要兼容多个平台的小程序、h5、ios app、android app包括nvue页面、vue页面。
> 所以希望在您修复bug并提交之前尽可能的去这些平台测试一下兼容性。最好能携带测试截图以方便审核。非常感谢
## 安装
#### **uni-app插件市场链接** —— [https://ext.dcloud.net.cn/plugin?name=uview-plus](https://ext.dcloud.net.cn/plugin?name=uview-plus)
请通过[官网安装文档](https://uview-plus.jiangruyi.com/components/install.html)了解更详细的内容
## 快速上手
请通过[快速上手](https://uview-plus.jiangruyi.com/components/quickstart.html)了解更详细的内容
## 使用方法
配置easycom规则后自动按需引入无需`import`组件,直接引用即可。
```html
<template>
<u-button text="按钮"></u-button>
</template>
```
## 版权信息
uview-plus遵循[MIT](https://en.wikipedia.org/wiki/MIT_License)开源协议意味着您无需支付任何费用也无需授权即可将uview-plus应用到您的产品中。

View File

@@ -0,0 +1,581 @@
## 3.3.632025-01-13
fix: cate-tab支持支付宝小程序
fix: textarea 修复 placeholder-style
fix: 修复在图片加载及加载失败时容器宽度
fix: waterfall组件报错Maximum recursive updates
## 3.3.622025-01-10
feat: sleder滑动选择器双滑块增加外层触发值的变动功能
fix: picker支持hasInput优化
## 3.3.612024-12-31
fix: 修复微信getSystemInfoSync接口废弃警告
fix: 'u-status-bar' symbol missing
## 3.3.602024-12-30
feat: 日期组件支持禁用
fix: ts定义修复 #600
feat: Tabs组件选中时增加一个active的class #595
## 3.3.592024-12-30
fix: Property "isH5" was accessed during render
## 3.3.582024-12-26
fix: slider组件change事件传参
## 3.3.572024-12-23
fix: slider组件change事件传参
feat: 更新u-picker组件增加当前选中class类名
## 3.3.562024-12-18
feat: 在u-alert组件中添加关闭事件
## 3.3.552024-12-17
add: swiper增加双向绑定
## 3.3.542024-12-11
add: qrcode支持props控制是否开启点击预览
add: 新增cate-tab垂直分类组件
## 3.3.532024-12-10
fix: 修复popup居中模式点击内容区域触发关闭
## 3.3.522024-12-09
add: notice-bar支持justifyContent属性
## 3.3.512024-12-09
add: radio增加label插槽
## 3.3.502024-12-05
fix: 优化popup等对禁止背景滚动机制
add: slider在弹窗使用示例
fix: card组件类名问题
## 3.3.492024-12-02
fix: 去除album多余的$u引用
fix: 优化图片组件兼容性
add: picker组件增加zIndex属性
add: text增加是否占满剩余空间属性
add: input颜色示例
## 3.3.482024-11-29
add: 文本行数限制样式提高到10行
del: 去除不跨端的inputmode
## 3.3.472024-11-28
fix: 时间选择器在hasInput模式下部分机型键盘弹出
## 3.3.462024-11-26
fix: 修复text传递事件参数
## 3.3.452024-11-24
add: navbar组件支持配置标题颜色
fix: 边框按钮警告类型下颜色变量使用错误
## 3.3.432024-11-18
fix: 支持瀑布流组件v-model置为[]
add: 新增字符串路径访问工具方法getValueByPath
add: 新增float-button悬浮按钮组件
## 3.3.422024-11-15
add: button组件支持stop参数阻止冒泡
## 3.3.412024-11-13
fix: u-radio-group invalid import
improvement: 优化图片组件宽高及修复事件event传递
## 3.3.402024-11-11
add: 组件radioGroup增加gap属性用于设置item间隔
fix: 修复H5全局导入
## 3.3.392024-11-04
fix: 修复相册组件
## 3.3.382024-11-04
fix: 修复视频预览报错 #510
add: album组件增加stop参数支持阻止事件冒泡
## 3.3.372024-10-21
fix: 修复因为修改组件名称前缀导致h5打包后$parent方法内找不到父组件的问题
fix: 修复datetime-picker选择2000年以前日期出错
## 3.3.362024-10-09
fix: toast 自动关闭
feat: 增加微信小程序用户昵称审核完毕回调及修改 ts 定义文件
## 3.3.352024-10-08
feat: modal和picker支持v-model:show双向绑定
feat: 支持checkbox使用slot自定义label后自带点击事件 #522
feat: swipe-action支持自动关闭特性及初始化打开状态
## 3.3.342024-09-23
feat: 支持toast设置duration值为-1时不自动关闭
## 3.3.332024-09-18
fix: 修复test.date('008')等验证结果不准确
## 3.3.322024-09-09
fix: u-keyboard名称冲突warning
## 3.3.312024-08-31
feat: qrcode初步支持nvue
## 3.3.302024-08-30
fix: slider兼容step为字符串类型
## 3.3.292024-08-30
fix: 修复tabs组件current参数为字符串处理逻辑
## 3.3.282024-08-26
fix: list组件滑动偏移量不一样取绝对值导致iOS下拉偏移量计算错误
## 3.3.272024-08-22
fix: 修复up-datetime-picker组件toolbarRightSlot定义缺失
fix: 修复FormItem的rules更新错误的问题
## 3.3.262024-08-22
fix: 批量注册全局组件优化
## 3.3.252024-08-21
fix: 修复slider在app-vue下样式问题
## 3.3.242024-08-19
fix: 修复时间选择器hasInput模式小程序不生效
feat: 支持H5导入所有组件
## 3.3.232024-08-17
feat: swipe-action增加closeAll方法
fix: 兼容tabs在某些场景下index小于0时自动设置为0
add: 通用mixin新增navTo页面跳转方法
## 3.3.212024-08-15
improvement: 优化二维码组件loading及支持预览与长按事件 #351
fix: 修复swipe-action自动关闭其它功能及组件卸载自动关闭
## 3.3.202024-08-15
refactor: props默认值文件移至组件文件夹内便于查找
## 3.3.192024-08-14
fix: 修复2被rpx兼容处理只在数字值生效
add: 增加swiper自定义插槽示例
## 3.3.182024-08-13
feat: 新增支持datetime-picker工具栏插槽及picker插槽支持修复
## 3.3.172024-08-12
feat: swiper组件增加默认slot便于自定义
feat: grid新增间隔参数
feat: picker新增toolbar-right和toolbar-bottom插槽
## 3.3.162024-08-12
fix: 解决swiper中title换行后多余的内容未被遮挡问题
fix: 修复迷你导航适配异形屏
## 3.3.152024-08-09
fix: 修复默认单位设置为rpx时一些组件高度间距异常
fix: 修复日历在rpx单位下布局异常
feat: code-input支持App端展示输入光标
## 3.3.142024-08-09
add: 增加box组件
add: 增加card卡片组件
## 3.3.132024-08-08
feat: input支持调用原生组件的focus和blur方法
improvement: grid-item条件编译优化
add: 新增迷你导航组件
## 3.3.122024-08-06
improvement: $u挂载时机调整便于打包分离chunk
fix: steps新增itemStyle属性名称冲突
## 3.3.112024-08-05
feat: 新增支持upload组件的deletable/maxCount/accept变更监听 #333
feat: 新增支持tabs在swiper中使用
feat: 新增FormItem支持独立设置验证规则rules
fix: 修复index-list未设置$slots.header时索引高亮失效
## 3.3.102024-08-02
fix: 修复index-list偶发的滑动最后一个索引报错top不存在
fix: 修复gird在QQ、抖音小程序下布局
feat: 优化step支持自定义样式prop
feat: action-sheet组件支持v-model:show双向绑定
fix: 小程序下steps和grid都统一采用grid布局
fix: 修复支付宝小程序下input类型为数字时双向绑定失效
feat : form 表单 validate 校验不通过后 error增加字段prop信息 #304
fix: form组件异步校异常验问题 #393
## 3.3.92024-08-01
fix: 优化获取nvue元素
feat: modal新增contentTextAlign设置文案对齐方式
fix: 修复NVUE下tabbar文字不显示 #458
feat: loading-page增加zIndex属性
fix: 相册在宽度较小时换行问题
feat: album相册增加自适应自动换行模式
feat: album相册增加图片尺寸单位prop
fix: 修复calendar日历月份居中
## 3.3.82024-07-31
feat: slider支持进度条任意位置触发按钮拖动
fix: 修复app-vue下modal标题不居中
fix: #459 TS setConfig 声明异常
feat: tabs组件增加longPress长按事件
feat: 新增showRight属性控制collapse右侧图标显隐
fix: 优化nvue下css警告
## 3.3.72024-07-29
feat: 支持IndexList组件支持在弹窗等场景下使用及联动优化
feat: popup组件支持v-model:show双向绑定
feat: 优化tabs的current双向绑定
fix: checkbox独立使用时checked赋初始值可以但是手动切换时值没有做双向绑定 #455
feat: slider组件支持区间双滑块
fix: toast 支持自定义图标?可传入了决对路径的 icon也没有用 #409
feat: form-item校验失败时 增加class方便自定义显示错误的展示方式 #394
fix: up-cell的required配置不生效 #395
fix: 横向滚动组件,微信小程序编译后会有警告 #415
fix: u-picker内部对默认值defaultIndex的监听 #425
feat: toast 组件支持遮掩层穿透 #417
fix: 兼容vue的slot编译bug #423
fix: upload 微信小程序 点击预览视频报错 #424
fix: u-number-box 组件修改【integer, decimalLength, min, max 】props时没有触发绑定值更新 #429
feat: Tabs组件能否支持自定义插槽 #439
feat: ActionSheet 可以配置最大高度吗, 我当做select使用了。 #445
fix: cursor-pointer优化
feat: 新版slider组件兼容NVUE改造
feat: 新增slider组件手动实现以支持样式自定义
perf补充TS声明提示信息
修复ActionSheet 操作菜单cancelText属性为空DOM节点还存在并且可以点击问题
fix: 去除预留的beforeDestroy兼容容易在某些sdk下不识别条件编译
## 3.3.62024-07-23
feat: u-album组件添加radius,shape参数定义参考当前u-image参数
fix: 修复了calendar组件title和日期title未垂直居中的问题
fix: update:modelValue缺失emit定义
## 3.3.52024-07-10
picker组件支持hasInput模式
## 3.3.42024-07-07
fix: input组件双向绑定问题 #419
lazy-load完善emit
优化通用小程序分享
## 3.3.22024-06-27
fix: 在Nvue环境中编译出现大量警告 #406
## 3.3.12024-06-27
u-button组件报错找不到button mixins #407
## 3.3.02024-06-27
feat: checkbox支持label设置slot
feat: modal增加customClass
feat: navbar、popup、tabs、text支持customClass
fix: cell组建缺少flex布局
fix: 修复微信小程序真机调试时快速输入出现文本回退问题
feat: tag增加默认slot
公共mixin改造为按需导入语法
refactor: 组件props混入mixin改造为按需导入语法
fix: u-tabbar 安卓手机点击按钮变蓝问题 #396
feat: upload组建增加extension属性
fix: upload组件参数mode添加left
fix: 修复阴影在非nvue时白色背景色不显示
## 3.2.242024-06-11
fix: 修复时间选择器confirm事件触发时机导致2次才会触发v-model更新
## 3.2.232024-05-30
fix: #378 H5 u-input 在表单中初始值为空也会触发一次 formValidate(this,"change")事件导致进入页面直接校验了一次
fix: #373 搜索组件up-search的@clear事件无效
fix: #372 ActionSheet 组件的取消按钮触发区域太小
## 3.2.222024-05-13
上传组件支持微信小程序预览视频
修复折叠面板右侧箭头不显示
修复uxp2px
## 3.2.212024-05-10
fix: loading-icon修复flex布局
## 3.2.202024-05-10
修复瀑布流大小写#355
## 3.2.192024-05-10
去除意外的文件引入
## 3.2.182024-05-09
fix: 349 popup 组件设置 zIndex 属性后,组件渲染异常#
feat: 搜索框增加adjustPosition属性
fix: #331增加u-action-sheet__cancel
优化mixin兼容性
feat: #326 up-list增加下拉刷新功能
fix: #319 优化up-tabs参数与定义匹配
fix: index-list组件微信小程序端使用自定义导航栏异常
fix: #285 pickerimmediateChange 写死为true
fix: #111 u-scroll-list组件,隐藏指示器后报错, 提示找不到ref
list增加微信小程序防抖配置
## 3.2.172024-05-08
fix: 支付宝小程序二维码渲染
## 3.2.162024-05-06
修复tabs中当前激活样式的undefined bug
fix: #341u-code 倒计时没结束前退出再次进入结束后退出界面再次进入重新开始倒计时bug
受到uni-app内置text样式影响修复
## 3.2.152024-04-28
优化时间选择器hasInput模式初始化值
## 3.2.142024-04-24
去除pleaseSetTranspileDependencies
http采用useStore
## 3.2.132024-04-22
修复modal标题样式
优化日期选择器hasInput模式宽度
## 3.2.122024-04-22
修复color应用
## 3.2.112024-04-18
修复import化带来的问题
## 3.2.102024-04-17
完善input清空事件App端失效的兼容性
修复日历组件二次打开后当前月份显示不正确
## 3.2.92024-04-16
组件内uni.$u用法改为import引入
规范化及兼容性增强
## 3.2.82024-04-15
修复up-tag语法错
## 3.2.72024-04-15
修复下拉菜单背景色在支付宝小程序无效
setConfig改为浅拷贝解决无法用import导入代替uni.$u.props设置
## 3.2.62024-04-14
修复某些情况下滑动单元格默认右侧按钮是展开的问题
## 3.2.52024-04-13
调整分段器尺寸及修复窗口大小改变时重新计算尺寸
多个组件支持cursor-pointer增强PC端体验
## 3.2.42024-04-12
初步支持typescript
## 3.2.32024-04-12
fix: 修复square属性在小程序下无效问题
fix:修复lastIndex异常导致的column异常问题
fix: alipayapp picker style
feat(button): 添加用户同意隐私协议事件回调
fix: input switch password
fix: 修复u-code组件keepRuning失效问题
feat: form-item添加labelPosition属性
新增dropdown组件
分段器支持内部current值
优化cell和action-sheet视觉大小
修复tabs文字换行
## 3.2.22024-04-11
修复换行符问题
## 3.2.12024-04-11
修复演示H5二维码
fix: #270 ReadMore 展开阅读更多内容变化兼容
fix: #238Calendar组件maxDate修改为不能小于minDate
checkbox支持独立使用
修复popup中在微信小程序中真机调试滚动失效
## 3.2.02024-04-10
修复轮播图在nvue显示
修复疑似u-slider名称被占用导致slider在App下不显示
解决微信小程序提示 Some selectors are not allowed in component wxss
示例中u-前缀统一为up-
增加瀑布流与图片懒加载组件
fix: #308修复tag组件缺失iconColor参数
fix: #297使用grid布局解决目前编译为抖音小程序无法开启virtualHost
## 3.1.522024-04-07
工具类方法调用import化改造
新增up-copy复制组件
## 3.1.512024-04-07
优化时间选择器自带输入框格式化显示
防止按钮文字换行
修复订单列表模板滑动
增加u-qrcode二维码组件
## 3.1.492024-03-27
日期时间组件支持自带输入框
fix: popup弹窗滚动穿透问题
fix: 修复小程序numberbox bug
## 3.1.482024-03-18
fix:[plugin:uni:pre-css] Unbalanced delimiter found in string
## 3.1.472024-03-18
fix: setConfig设置组件默认参数无效问题
fix: 修复自定义图标无效问题
feat: 增加u-form-item单独设置规则变量
fix#293小程序是自定义导航栏的时候即传了customNavHeight的时候会出现跳转偏移的情况
## 3.1.462024-01-29
beforeUnmount
## 3.1.452024-01-24
fix: #262ext组件为超链接的情况下size属性不生效
fix: #263最新版本3.1.42中微信小程序u-swipe-action-item报错
fix: #224最新版本3.1.42中微信小程序u-swipe-action-item报错
fix: #263支持支付宝小程序
fix: #261u-input在直接修改v-model的绑定值时每隔一次会无法出发change事件
优化折叠面板兼容微信小程序
## 3.1.422024-01-15
修复u-number-box默认值0时在小程序不显示值
优化u-code的timer判断
优化支付宝小程序下textarea字数统计兼容
优化u-calendar
## 3.1.412023-11-18
#215优化u-cell图标容器间距问题
## 3.1.402023-11-16
修复u-slider双向绑定
## 3.1.392023-11-10
修复头条小程序不支持env(safe-area-inset-bottom)
优化#201u-grid 指定列数导致闪烁
#193IndexList 索引列表 高度错误
其他优化
## 3.1.382023-10-08
修复u-slider
## 3.1.372023-09-13
完善emits定义及修复code-input双向数据绑定
## 3.1.362023-08-08
修复富文本事件名称大小写
## 3.1.352023-08-02
修复编译到支付宝小程序u-form报错
## 3.1.342023-07-27
修复App打包uni.$u.mpMixin方式sdk暂时不支持导致报错
## 3.1.332023-07-13
修复弹窗进入动画、模板页面样式等
## 3.1.312023-07-11
修复dayjs引用
## 3.0.82022-07-12
修复u-tag默认宽度撑满容器
## 3.0.72022-07-12
修复u-navbar自定义插槽演示示例
## 3.0.62022-07-11
修复u-image缺少emits申明
## 3.0.52022-07-11
修复u-upload缺少emits申明
## 3.0.42022-07-10
修复u-textarea/u-input/u-datetime-picker/u-number-box/u-radio-group/u-switch/u-rate在vue3下数据绑定
## 3.0.32022-07-09
启用自建演示二维码
## 3.0.22022-07-09
修复dayjs/clipboard等导致打包报错
## 3.0.12022-07-09
增加插件市场地址
## 3.0.02022-07-09
# uview-plus(vue3)初步发布

View File

@@ -0,0 +1,80 @@
<template>
<uvForm
ref="uForm"
:model="model"
:rules="rules"
:errorType="errorType"
:borderBottom="borderBottom"
:labelPosition="labelPosition"
:labelWidth="labelWidth"
:labelAlign="labelAlign"
:labelStyle="labelStyle"
:customStyle="customStyle"
>
<slot />
</uvForm>
</template>
<script>
/**
* 此组件存在的理由是在nvue下u-form被uni-app官方占用了u-form在nvue中相当于form组件
* 所以在nvue下取名为u--form内部其实还是u-form.vue只不过做一层中转
*/
import uvForm from '../u-form/u-form.vue';
import { props } from '../u-form/props.js';
import { mpMixin } from '../../libs/mixin/mpMixin';
import { mixin } from '../../libs/mixin/mixin';
export default {
// #ifdef MP-WEIXIN
name: 'u-form',
// #endif
// #ifndef MP-WEIXIN
name: 'u--form',
// #endif
mixins: [mpMixin, props, mixin],
components: {
uvForm
},
created() {
this.children = []
},
methods: {
// 手动设置校验的规则,如果规则中有函数的话,微信小程序中会过滤掉,所以只能手动调用设置规则
setRules(rules) {
this.$refs.uForm.setRules(rules)
},
validate() {
/**
* 在微信小程序中通过this.$parent拿到的父组件是u--form而不是其内嵌的u-form
* 导致在u-form组件中拿不到对应的children数组从而校验无效所以这里每次调用u-form组件中的
* 对应方法的时候在小程序中都先将u--form的children赋值给u-form中的children
*/
// #ifdef MP-WEIXIN
this.setMpData()
// #endif
return this.$refs.uForm.validate()
},
validateField(value, callback) {
// #ifdef MP-WEIXIN
this.setMpData()
// #endif
return this.$refs.uForm.validateField(value, callback)
},
resetFields() {
// #ifdef MP-WEIXIN
this.setMpData()
// #endif
return this.$refs.uForm.resetFields()
},
clearValidate(props) {
// #ifdef MP-WEIXIN
this.setMpData()
// #endif
return this.$refs.uForm.clearValidate(props)
},
setMpData() {
this.$refs.uForm.children = this.children
}
},
}
</script>

View File

@@ -0,0 +1,50 @@
<template>
<uvImage
:src="src"
:mode="mode"
:width="width"
:height="height"
:shape="shape"
:radius="radius"
:lazyLoad="lazyLoad"
:showMenuByLongpress="showMenuByLongpress"
:loadingIcon="loadingIcon"
:errorIcon="errorIcon"
:showLoading="showLoading"
:showError="showError"
:fade="fade"
:webp="webp"
:duration="duration"
:bgColor="bgColor"
:customStyle="customStyle"
@click="$emit('click')"
@error="$emit('error')"
@load="$emit('load')"
>
<template v-slot:loading>
<slot name="loading"></slot>
</template>
<template v-slot:error>
<slot name="error"></slot>
</template>
</uvImage>
</template>
<script>
/**
* 此组件存在的理由是在nvue下u-image被uni-app官方占用了u-image在nvue中相当于image组件
* 所以在nvue下取名为u--image内部其实还是u-iamge.vue只不过做一层中转
*/
import uvImage from '../u-image/u-image.vue';
import { props } from '../u-image/props.js';
import { mpMixin } from '../../libs/mixin/mpMixin';
import { mixin } from '../../libs/mixin/mixin';
export default {
name: 'u--image',
mixins: [mpMixin, props, mixin],
components: {
uvImage
},
emits: ['click', 'error', 'load']
}
</script>

View File

@@ -0,0 +1,74 @@
<template>
<uvInput
<!-- #ifdef VUE2 -->
:value="value"
@input="e => $emit('input', e)"
<!-- #endif -->
<!-- #ifdef VUE3 -->
:modelValue="modelValue"
@update:modelValue="e => $emit('update:modelValue', e)"
<!-- #endif -->
:type="type"
:fixed="fixed"
:disabled="disabled"
:disabledColor="disabledColor"
:clearable="clearable"
:password="password"
:maxlength="maxlength"
:placeholder="placeholder"
:placeholderClass="placeholderClass"
:placeholderStyle="placeholderStyle"
:showWordLimit="showWordLimit"
:confirmType="confirmType"
:confirmHold="confirmHold"
:holdKeyboard="holdKeyboard"
:focus="focus"
:autoBlur="autoBlur"
:disableDefaultPadding="disableDefaultPadding"
:cursor="cursor"
:cursorSpacing="cursorSpacing"
:selectionStart="selectionStart"
:selectionEnd="selectionEnd"
:adjustPosition="adjustPosition"
:inputAlign="inputAlign"
:fontSize="fontSize"
:color="color"
:prefixIcon="prefixIcon"
:suffixIcon="suffixIcon"
:suffixIconStyle="suffixIconStyle"
:prefixIconStyle="prefixIconStyle"
:border="border"
:readonly="readonly"
:shape="shape"
:customStyle="customStyle"
:formatter="formatter"
:ignoreCompositionEvent="ignoreCompositionEvent"
>
<!-- #ifdef MP -->
<slot name="prefix"></slot>
<slot name="suffix"></slot>
<!-- #endif -->
<!-- #ifndef MP -->
<slot name="prefix" slot="prefix"></slot>
<slot name="suffix" slot="suffix"></slot>
<!-- #endif -->
</uvInput>
</template>
<script>
/**
* 此组件存在的理由是在nvue下u-input被uni-app官方占用了u-input在nvue中相当于input组件
* 所以在nvue下取名为u--input内部其实还是u-input.vue只不过做一层中转
*/
import uvInput from '../u-input/u-input.vue';
import { props } from '../u-input/props.js';
import { mpMixin } from '../../libs/mixin/mpMixin';
import { mixin } from '../../libs/mixin/mixin';
export default {
name: 'u--input',
mixins: [mpMixin, props, mixin],
components: {
uvInput
},
}
</script>

View File

@@ -0,0 +1,45 @@
<template>
<uvText
:type="type"
:show="show"
:text="text"
:prefixIcon="prefixIcon"
:suffixIcon="suffixIcon"
:mode="mode"
:href="href"
:format="format"
:call="call"
:openType="openType"
:bold="bold"
:block="block"
:lines="lines"
:color="color"
:decoration="decoration"
:size="size"
:iconStyle="iconStyle"
:margin="margin"
:lineHeight="lineHeight"
:align="align"
:wordWrap="wordWrap"
:customStyle="customStyle"
></uvText>
</template>
<script>
/**
* 此组件存在的理由是在nvue下u-text被uni-app官方占用了u-text在nvue中相当于input组件
* 所以在nvue下取名为u--input内部其实还是u-text.vue只不过做一层中转
* 不使用v-bind="$attrs",而是分开独立写传参,是因为微信小程序不支持此写法
*/
import uvText from "../u-text/u-text.vue";
import { props } from "../u-text/props.js";
import { mpMixin } from '../../libs/mixin/mpMixin.js'
import { mixin } from '../../libs/mixin/mixin.js'
export default {
name: "u--text",
mixins: [mpMixin, mixin, props,],
components: {
uvText,
},
};
</script>

View File

@@ -0,0 +1,47 @@
<template>
<uvTextarea
:value="value"
:modelValue="modelValue"
:placeholder="placeholder"
:height="height"
:confirmType="confirmType"
:disabled="disabled"
:count="count"
:focus="focus"
:autoHeight="autoHeight"
:fixed="fixed"
:cursorSpacing="cursorSpacing"
:cursor="cursor"
:showConfirmBar="showConfirmBar"
:selectionStart="selectionStart"
:selectionEnd="selectionEnd"
:adjustPosition="adjustPosition"
:disableDefaultPadding="disableDefaultPadding"
:holdKeyboard="holdKeyboard"
:maxlength="maxlength"
:border="border"
:customStyle="customStyle"
:formatter="formatter"
:ignoreCompositionEvent="ignoreCompositionEvent"
@input="e => $emit('input', e)"
@update:modelValue="e => $emit('update:modelValue', e)"
></uvTextarea>
</template>
<script>
/**
* 此组件存在的理由是在nvue下u--textarea被uni-app官方占用了u-textarea在nvue中相当于textarea组件
* 所以在nvue下取名为u--textarea内部其实还是u-textarea.vue只不过做一层中转
*/
import uvTextarea from '../u-textarea/u-textarea.vue';
import { props } from '../u-textarea/props.js';
import { mpMixin } from '../../libs/mixin/mpMixin';
import { mixin } from '../../libs/mixin/mixin';
export default {
name: 'u--textarea',
mixins: [mpMixin, props, mixin],
components: {
uvTextarea
},
}
</script>

View File

@@ -0,0 +1,26 @@
/*
* @Author : LQ
* @Description :
* @version : 1.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:44:35
* @FilePath : /u-view2.0/uview-ui/libs/config/props/actionSheet.js
*/
export default {
// action-sheet组件
actionSheet: {
show: false,
title: '',
description: '',
actions: [],
index: '',
cancelText: '',
closeOnClickAction: true,
safeAreaInsetBottom: true,
openType: '',
closeOnClickOverlay: true,
round: 0,
wrapMaxHeight: '600px'
}
}

View File

@@ -0,0 +1,62 @@
import { defineMixin } from '../../libs/vue'
import defProps from '../../libs/config/props.js'
export const props = defineMixin({
props: {
// 操作菜单是否展示 默认false
show: {
type: Boolean,
default: () => defProps.actionSheet.show
},
// 标题
title: {
type: String,
default: () => defProps.actionSheet.title
},
// 选项上方的描述信息
description: {
type: String,
default: () => defProps.actionSheet.description
},
// 数据
actions: {
type: Array,
default: () => defProps.actionSheet.actions
},
// 取消按钮的文字,不为空时显示按钮
cancelText: {
type: String,
default: () => defProps.actionSheet.cancelText
},
// 点击某个菜单项时是否关闭弹窗
closeOnClickAction: {
type: Boolean,
default: () => defProps.actionSheet.closeOnClickAction
},
// 处理底部安全区默认true
safeAreaInsetBottom: {
type: Boolean,
default: () => defProps.actionSheet.safeAreaInsetBottom
},
// 小程序的打开方式
openType: {
type: String,
default: () => defProps.actionSheet.openType
},
// 点击遮罩是否允许关闭 (默认true)
closeOnClickOverlay: {
type: Boolean,
default: () => defProps.actionSheet.closeOnClickOverlay
},
// 圆角值
round: {
type: [Boolean, String, Number],
default: () => defProps.actionSheet.round
},
// 选项区域最大高度
wrapMaxHeight: {
type: [String],
default: () => defProps.actionSheet.wrapMaxHeight
},
}
})

View File

@@ -0,0 +1,283 @@
<template>
<u-popup
:show="show"
mode="bottom"
@close="closeHandler"
:safeAreaInsetBottom="safeAreaInsetBottom"
:round="round"
>
<view class="u-action-sheet">
<view
class="u-action-sheet__header"
v-if="title"
>
<text class="u-action-sheet__header__title u-line-1">{{title}}</text>
<view
class="u-action-sheet__header__icon-wrap"
@tap.stop="cancel"
>
<u-icon
name="close"
size="17"
color="#c8c9cc"
bold
></u-icon>
</view>
</view>
<text
class="u-action-sheet__description"
:style="[{
marginTop: `${title && description ? 0 : '18px'}`
}]"
v-if="description"
>{{description}}</text>
<slot>
<u-line v-if="description"></u-line>
<scroll-view scroll-y class="u-action-sheet__item-wrap" :style="{maxHeight: wrapMaxHeight}">
<view :key="index" v-for="(item, index) in actions">
<!-- #ifdef MP -->
<button
class="u-reset-button"
:openType="item.openType"
@getuserinfo="onGetUserInfo"
@contact="onContact"
@getphonenumber="onGetPhoneNumber"
@error="onError"
@launchapp="onLaunchApp"
@opensetting="onOpenSetting"
:lang="lang"
:session-from="sessionFrom"
:send-message-title="sendMessageTitle"
:send-message-path="sendMessagePath"
:send-message-img="sendMessageImg"
:show-message-card="showMessageCard"
:app-parameter="appParameter"
@tap="selectHandler(index)"
:hover-class="!item.disabled && !item.loading ? 'u-action-sheet--hover' : ''"
>
<!-- #endif -->
<view
class="u-action-sheet__item-wrap__item"
@tap.stop="selectHandler(index)"
:hover-class="!item.disabled && !item.loading ? 'u-action-sheet--hover' : ''"
:hover-stay-time="150"
>
<template v-if="!item.loading">
<text
class="u-action-sheet__item-wrap__item__name"
:style="[itemStyle(index)]"
>{{ item.name }}</text>
<text
v-if="item.subname"
class="u-action-sheet__item-wrap__item__subname"
>{{ item.subname }}</text>
</template>
<u-loading-icon
v-else
custom-class="van-action-sheet__loading"
size="18"
mode="circle"
/>
</view>
<!-- #ifdef MP -->
</button>
<!-- #endif -->
<u-line v-if="index !== actions.length - 1"></u-line>
</view>
</scroll-view>
</slot>
<u-gap
bgColor="#eaeaec"
height="6"
v-if="cancelText"
></u-gap>
<view class="u-action-sheet__item-wrap__item u-action-sheet__cancel"
hover-class="u-action-sheet--hover" @tap="cancel" v-if="cancelText">
<text
@touchmove.stop.prevent
:hover-stay-time="150"
class="u-action-sheet__cancel-text"
>{{cancelText}}</text>
</view>
</view>
</u-popup>
</template>
<script>
import { openType } from '../../libs/mixin/openType'
import { buttonMixin } from '../../libs/mixin/button'
import { props } from './props';
import { mpMixin } from '../../libs/mixin/mpMixin';
import { mixin } from '../../libs/mixin/mixin';
import { addUnit } from '../../libs/function/index';
/**
* ActionSheet 操作菜单
* @description 本组件用于从底部弹出一个操作菜单供用户选择并返回结果。本组件功能类似于uni的uni.showActionSheetAPI配置更加灵活所有平台都表现一致。
* @tutorial https://ijry.github.io/uview-plus/components/actionSheet.html
*
* @property {Boolean} show 操作菜单是否展示 (默认 false
* @property {String} title 操作菜单标题
* @property {String} description 选项上方的描述信息
* @property {Array<Object>} actions 按钮的文字数组,见官方文档示例
* @property {String} cancelText 取消按钮的提示文字,不为空时显示按钮
* @property {Boolean} closeOnClickAction 点击某个菜单项时是否关闭弹窗 (默认 true
* @property {Boolean} safeAreaInsetBottom 处理底部安全区 (默认 true
* @property {String} openType 小程序的打开方式 (contact | launchApp | getUserInfo | openSetting getPhoneNumber error )
* @property {Boolean} closeOnClickOverlay 点击遮罩是否允许关闭 (默认 true )
* @property {Number|String} round 圆角值,默认无圆角 (默认 0 )
* @property {String} lang 指定返回用户信息的语言zh_CN 简体中文zh_TW 繁体中文en 英文
* @property {String} sessionFrom 会话来源openType="contact"时有效
* @property {String} sendMessageTitle 会话内消息卡片标题openType="contact"时有效
* @property {String} sendMessagePath 会话内消息卡片点击跳转小程序路径openType="contact"时有效
* @property {String} sendMessageImg 会话内消息卡片图片openType="contact"时有效
* @property {Boolean} showMessageCard 是否显示会话内消息卡片,设置此参数为 true用户进入客服会话会在右下角显示"可能要发送的小程序"提示用户点击后可以快速发送小程序消息openType="contact"时有效 (默认 false
* @property {String} appParameter 打开 APP 时,向 APP 传递的参数openType=launchApp 时有效
*
* @event {Function} select 点击ActionSheet列表项时触发
* @event {Function} close 点击取消按钮时触发
* @event {Function} getuserinfo 用户点击该按钮时,会返回获取到的用户信息,回调的 detail 数据与 wx.getUserInfo 返回的一致openType="getUserInfo"时有效
* @event {Function} contact 客服消息回调openType="contact"时有效
* @event {Function} getphonenumber 获取用户手机号回调openType="getPhoneNumber"时有效
* @event {Function} error 当使用开放能力时发生错误的回调openType="error"时有效
* @event {Function} launchapp 打开 APP 成功的回调openType="launchApp"时有效
* @event {Function} opensetting 在打开授权设置页后回调openType="openSetting"时有效
* @example <u-action-sheet :actions="list" :title="title" :show="show"></u-action-sheet>
*/
export default {
name: "u-action-sheet",
// 一些props参数和methods方法通过mixin混入因为其他文件也会用到
mixins: [openType, buttonMixin, mixin, props],
data() {
return {
}
},
computed: {
// 操作项目的样式
itemStyle() {
return (index) => {
let style = {};
if (this.actions[index].color) style.color = this.actions[index].color
if (this.actions[index].fontSize) style.fontSize = addUnit(this.actions[index].fontSize)
// 选项被禁用的样式
if (this.actions[index].disabled) style.color = '#c0c4cc'
return style;
}
},
},
emits: ["close", "select", "update:show"],
methods: {
closeHandler() {
// 允许点击遮罩关闭时才发出close事件
if(this.closeOnClickOverlay) {
this.$emit('update:show', false)
this.$emit('close')
}
},
// 点击取消按钮
cancel() {
this.$emit('update:show', false)
this.$emit('close')
},
selectHandler(index) {
const item = this.actions[index]
if (item && !item.disabled && !item.loading) {
this.$emit('select', item)
if (this.closeOnClickAction) {
this.$emit('update:show', false)
this.$emit('close')
}
}
},
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
$u-action-sheet-reset-button-width:100% !default;
$u-action-sheet-title-font-size: 16px !default;
$u-action-sheet-title-padding: 12px 30px !default;
$u-action-sheet-title-color: $u-main-color !default;
$u-action-sheet-header-icon-wrap-right:15px !default;
$u-action-sheet-header-icon-wrap-top:15px !default;
$u-action-sheet-description-font-size:13px !default;
$u-action-sheet-description-color:14px !default;
$u-action-sheet-description-margin: 18px 15px !default;
$u-action-sheet-item-wrap-item-padding:17px !default;
$u-action-sheet-item-wrap-name-font-size:16px !default;
$u-action-sheet-item-wrap-subname-font-size:13px !default;
$u-action-sheet-item-wrap-subname-color: #c0c4cc !default;
$u-action-sheet-item-wrap-subname-margin-top:10px !default;
$u-action-sheet-cancel-text-font-size:16px !default;
$u-action-sheet-cancel-text-color:$u-content-color !default;
$u-action-sheet-cancel-text-font-size:15px !default;
$u-action-sheet-cancel-text-hover-background-color:rgb(242, 243, 245) !default;
.u-reset-button {
width: $u-action-sheet-reset-button-width;
}
.u-action-sheet {
text-align: center;
&__header {
position: relative;
padding: $u-action-sheet-title-padding;
&__title {
font-size: $u-action-sheet-title-font-size;
color: $u-action-sheet-title-color;
font-weight: bold;
text-align: center;
}
&__icon-wrap {
position: absolute;
right: $u-action-sheet-header-icon-wrap-right;
top: $u-action-sheet-header-icon-wrap-top;
}
}
&__description {
font-size: $u-action-sheet-description-font-size;
color: $u-tips-color;
margin: $u-action-sheet-description-margin;
text-align: center;
}
&__item-wrap {
&__item {
padding: $u-action-sheet-item-wrap-item-padding;
@include flex;
align-items: center;
justify-content: center;
flex-direction: column;
&__name {
font-size: $u-action-sheet-item-wrap-name-font-size;
color: $u-main-color;
text-align: center;
}
&__subname {
font-size: $u-action-sheet-item-wrap-subname-font-size;
color: $u-action-sheet-item-wrap-subname-color;
margin-top: $u-action-sheet-item-wrap-subname-margin-top;
text-align: center;
}
}
}
&__cancel-text {
font-size: $u-action-sheet-cancel-text-font-size;
color: $u-action-sheet-cancel-text-color;
text-align: center;
// padding: $u-action-sheet-cancel-text-font-size;
}
&--hover {
background-color: $u-action-sheet-cancel-text-hover-background-color;
}
}
</style>

View File

@@ -0,0 +1,28 @@
/*
* @Author : LQ
* @Description :
* @version : 1.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:47:24
* @FilePath : /u-view2.0/uview-ui/libs/config/props/album.js
*/
export default {
// album 组件
album: {
urls: [],
keyName: '',
singleSize: 180,
multipleSize: 70,
space: 6,
singleMode: 'scaleToFill',
multipleMode: 'aspectFill',
maxCount: 9,
previewFullImage: true,
rowCount: 3,
showMore: true,
autoWrap: false,
unit: 'px',
stop: true,
}
}

View File

@@ -0,0 +1,86 @@
import { defineMixin } from '../../libs/vue'
import defProps from '../../libs/config/props.js'
export const props = defineMixin({
props: {
// 图片地址Array<String>|Array<Object>形式
urls: {
type: Array,
default: () => defProps.album.urls
},
// 指定从数组的对象元素中读取哪个属性作为图片地址
keyName: {
type: String,
default: () => defProps.album.keyName
},
// 单图时,图片长边的长度
singleSize: {
type: [String, Number],
default: () => defProps.album.singleSize
},
// 多图时,图片边长
multipleSize: {
type: [String, Number],
default: () => defProps.album.multipleSize
},
// 多图时,图片水平和垂直之间的间隔
space: {
type: [String, Number],
default: () => defProps.album.space
},
// 单图时,图片缩放裁剪的模式
singleMode: {
type: String,
default: () => defProps.album.singleMode
},
// 多图时,图片缩放裁剪的模式
multipleMode: {
type: String,
default: () => defProps.album.multipleMode
},
// 最多展示的图片数量,超出时最后一个位置将会显示剩余图片数量
maxCount: {
type: [String, Number],
default: () => defProps.album.maxCount
},
// 是否可以预览图片
previewFullImage: {
type: Boolean,
default: () => defProps.album.previewFullImage
},
// 每行展示图片数量如设置singleSize和multipleSize将会无效
rowCount: {
type: [String, Number],
default: () => defProps.album.rowCount
},
// 超出maxCount时是否显示查看更多的提示
showMore: {
type: Boolean,
default: () => defProps.album.showMore
},
// 图片形状circle-圆形square-方形
shape: {
type: String,
default: () => defProps.image.shape
},
// 圆角,单位任意
radius: {
type: [String, Number],
default: () => defProps.image.radius
},
// 自适应换行
autoWrap: {
type: Boolean,
default: () => defProps.album.autoWrap
},
// 单位
unit: {
type: [String],
default: () => defProps.album.unit
},
// 阻止点击冒泡
stop: {
type: Boolean,
default: () => defProps.album.stop
}
}
})

View File

@@ -0,0 +1,279 @@
<template>
<view class="u-album">
<view
class="u-album__row"
ref="u-album__row"
v-for="(arr, index) in showUrls"
:forComputedUse="albumWidth"
:key="index"
:style="{flexWrap: autoWrap ? 'wrap' : 'nowrap'}"
>
<view
class="u-album__row__wrapper"
v-for="(item, index1) in arr"
:key="index1"
:style="[imageStyle(index + 1, index1 + 1)]"
@tap="previewFullImage ? onPreviewTap($event, getSrc(item)) : ''"
>
<image
:src="getSrc(item)"
:mode="
urls.length === 1
? imageHeight > 0
? singleMode
: 'widthFix'
: multipleMode
"
:style="[
{
width: imageWidth,
height: imageHeight,
borderRadius: shape == 'circle' ? '10000px' : addUnit(radius)
}
]"
></image>
<view
v-if="
showMore &&
urls.length > rowCount * showUrls.length &&
index === showUrls.length - 1 &&
index1 === showUrls[showUrls.length - 1].length - 1
"
class="u-album__row__wrapper__text"
:style="{
borderRadius: shape == 'circle' ? '50%' : addUnit(radius),
}"
>
<up-text
:text="`+${urls.length - maxCount}`"
color="#fff"
:size="multipleSize * 0.3"
align="center"
customStyle="justify-content: center"
></up-text>
</view>
</view>
</view>
</view>
</template>
<script>
import { props } from './props';
import { mpMixin } from '../../libs/mixin/mpMixin';
import { mixin } from '../../libs/mixin/mixin';
import { addUnit, sleep } from '../../libs/function/index';
import test from '../../libs/function/test';
// #ifdef APP-NVUE
// 由于weex为阿里的KPI业绩考核的产物所以不支持百分比单位这里需要通过dom查询组件的宽度
const dom = uni.requireNativePlugin('dom')
// #endif
/**
* Album 相册
* @description 本组件提供一个类似相册的功能,让开发者开发起来更加得心应手。减少重复的模板代码
* @tutorial https://ijry.github.io/uview-plus/components/album.html
*
* @property {Array} urls 图片地址列表 Array<String>|Array<Object>形式
* @property {String} keyName 指定从数组的对象元素中读取哪个属性作为图片地址
* @property {String | Number} singleSize 单图时,图片长边的长度 (默认 180
* @property {String | Number} multipleSize 多图时,图片边长 (默认 70
* @property {String | Number} space 多图时,图片水平和垂直之间的间隔 (默认 6
* @property {String} singleMode 单图时,图片缩放裁剪的模式 (默认 'scaleToFill'
* @property {String} multipleMode 多图时,图片缩放裁剪的模式 (默认 'aspectFill'
* @property {String | Number} maxCount 取消按钮的提示文字 (默认 9
* @property {Boolean} previewFullImage 是否可以预览图片 (默认 true
* @property {String | Number} rowCount 每行展示图片数量如设置singleSize和multipleSize将会无效 (默认 3
* @property {Boolean} showMore 超出maxCount时是否显示查看更多的提示 (默认 true
* @property {String} shape 图片形状circle-圆形square-方形 (默认 'square'
* @property {String | Number} radius 圆角值单位任意如果为数值则为px单位 (默认 0
* @property {Boolean} autoWrap 自适应换行模式不受rowCount限制图片会自动换行 (默认 false
* @property {String} unit 图片单位 (默认 px
* @event {Function} albumWidth 某些特殊的情况下,需要让文字与相册的宽度相等,这里事件的形式对外发送 (回调参数 width
* @example <u-album :urls="urls2" @albumWidth="width => albumWidth = width" multipleSize="68" ></u-album>
*/
export default {
name: 'u-album',
mixins: [mpMixin, mixin, props],
data() {
return {
// 单图的宽度
singleWidth: 0,
// 单图的高度
singleHeight: 0,
// 单图时,如果无法获取图片的尺寸信息,让图片宽度默认为容器的一定百分比
singlePercent: 0.6
}
},
watch: {
urls: {
immediate: true,
handler(newVal) {
if (newVal.length === 1) {
this.getImageRect()
}
}
}
},
emits: ["albumWidth"],
computed: {
imageStyle() {
return (index1, index2) => {
const { space, rowCount, multipleSize, urls } = this,
rowLen = this.showUrls.length,
allLen = this.urls.length
const style = {
marginRight: addUnit(space),
marginBottom: addUnit(space)
}
// 如果为最后一行,则每个图片都无需下边框
if (index1 === rowLen && !this.autoWrap) style.marginBottom = 0
// 每行的最右边一张和总长度的最后一张无需右边框
if (!this.autoWrap) {
if (
index2 === rowCount ||
(index1 === rowLen &&
index2 === this.showUrls[index1 - 1].length)
)
style.marginRight = 0
}
return style
}
},
// 将数组划分为二维数组
showUrls() {
if (this.autoWrap) {
return [ this.urls.slice(0, this.maxCount) ];
} else {
const arr = []
this.urls.map((item, index) => {
// 限制最大展示数量
if (index + 1 <= this.maxCount) {
// 计算该元素为第几个素组内
const itemIndex = Math.floor(index / this.rowCount)
// 判断对应的索引是否存在
if (!arr[itemIndex]) {
arr[itemIndex] = []
}
arr[itemIndex].push(item)
}
})
return arr
}
},
imageWidth() {
return addUnit(
this.urls.length === 1 ? this.singleWidth : this.multipleSize, this.unit
)
},
imageHeight() {
return addUnit(
this.urls.length === 1 ? this.singleHeight : this.multipleSize, this.unit
)
},
// 此变量无实际用途仅仅是为了利用computed特性让其在urls长度等变化时重新计算图片的宽度
// 因为用户在某些特殊的情况下,需要让文字与相册的宽度相等,所以这里事件的形式对外发送
albumWidth() {
let width = 0
if (this.urls.length === 1) {
width = this.singleWidth
} else {
width =
this.showUrls[0].length * this.multipleSize +
this.space * (this.showUrls[0].length - 1)
}
this.$emit('albumWidth', width)
return width
}
},
methods: {
addUnit,
// 预览图片
onPreviewTap(e, url) {
const urls = this.urls.map((item) => {
return this.getSrc(item)
})
uni.previewImage({
current: url,
urls
})
// 是否阻止事件传播
this.stop && this.preventEvent(e)
},
// 获取图片的路径
getSrc(item) {
return test.object(item)
? (this.keyName && item[this.keyName]) || item.src
: item
},
// 单图时,获取图片的尺寸
// 在小程序中需要将网络图片的的域名添加到小程序的download域名才可能获取尺寸
// 在没有添加的情况下,让单图宽度默认为盒子的一定宽度(singlePercent)
getImageRect() {
const src = this.getSrc(this.urls[0])
uni.getImageInfo({
src,
success: (res) => {
// 判断图片横向还是竖向展示方式
const isHorizotal = res.width >= res.height
this.singleWidth = isHorizotal
? this.singleSize
: (res.width / res.height) * this.singleSize
this.singleHeight = !isHorizotal
? this.singleSize
: (res.height / res.width) * this.singleWidth
},
fail: () => {
this.getComponentWidth()
}
})
},
// 获取组件的宽度
async getComponentWidth() {
// 延时一定时间以获取dom尺寸
await sleep(30)
// #ifndef APP-NVUE
this.$uGetRect('.u-album__row').then((size) => {
this.singleWidth = size.width * this.singlePercent
})
// #endif
// #ifdef APP-NVUE
// 这里ref="u-album__row"所在的标签为通过for循环出来导致this.$refs['u-album__row']是一个数组
const ref = this.$refs['u-album__row'][0]
ref &&
dom.getComponentRect(ref, (res) => {
this.singleWidth = res.size.width * this.singlePercent
})
// #endif
}
}
}
</script>
<style lang="scss" scoped>
@import '../../libs/css/components.scss';
.u-album {
@include flex(column);
&__row {
@include flex(row);
&__wrapper {
position: relative;
&__text {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.3);
@include flex(row);
justify-content: center;
align-items: center;
}
}
}
}
</style>

View File

@@ -0,0 +1,22 @@
/*
* @Author : LQ
* @Description :
* @version : 1.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:48:53
* @FilePath : /u-view2.0/uview-ui/libs/config/props/alert.js
*/
export default {
// alert警告组件
alert: {
title: '',
type: 'warning',
description: '',
closable: false,
showIcon: false,
effect: 'light',
center: false,
fontSize: 14
}
}

View File

@@ -0,0 +1,46 @@
import { defineMixin } from '../../libs/vue'
import defProps from '../../libs/config/props.js'
export const props = defineMixin({
props: {
// 显示文字
title: {
type: String,
default: () => defProps.alert.title
},
// 主题success/warning/info/error
type: {
type: String,
default: () => defProps.alert.type
},
// 辅助性文字
description: {
type: String,
default: () => defProps.alert.description
},
// 是否可关闭
closable: {
type: Boolean,
default: () => defProps.alert.closable
},
// 是否显示图标
showIcon: {
type: Boolean,
default: () => defProps.alert.showIcon
},
// 浅或深色调light-浅色dark-深色
effect: {
type: String,
default: () => defProps.alert.effect
},
// 文字是否居中
center: {
type: Boolean,
default: () => defProps.alert.center
},
// 字体大小
fontSize: {
type: [String, Number],
default: () => defProps.alert.fontSize
}
}
})

View File

@@ -0,0 +1,251 @@
<template>
<u-transition
mode="fade"
:show="show"
>
<view
class="u-alert"
:class="[`u-alert--${type}--${effect}`]"
@tap.stop="clickHandler"
:style="[addStyle(customStyle)]"
>
<view
class="u-alert__icon"
v-if="showIcon"
>
<u-icon
:name="iconName"
size="18"
:color="iconColor"
></u-icon>
</view>
<view
class="u-alert__content"
:style="[{
paddingRight: closable ? '20px' : 0
}]"
>
<text
class="u-alert__content__title"
v-if="title"
:style="[{
fontSize: addUnit(fontSize),
textAlign: center ? 'center' : 'left'
}]"
:class="[effect === 'dark' ? 'u-alert__text--dark' : `u-alert__text--${type}--light`]"
>{{ title }}</text>
<text
class="u-alert__content__desc"
v-if="description"
:style="[{
fontSize: addUnit(fontSize),
textAlign: center ? 'center' : 'left'
}]"
:class="[effect === 'dark' ? 'u-alert__text--dark' : `u-alert__text--${type}--light`]"
>{{ description }}</text>
</view>
<view
class="u-alert__close"
v-if="closable"
@tap.stop="closeHandler"
>
<u-icon
name="close"
:color="iconColor"
size="15"
></u-icon>
</view>
</view>
</u-transition>
</template>
<script>
import { props } from './props';
import { mpMixin } from '../../libs/mixin/mpMixin';
import { mixin } from '../../libs/mixin/mixin';
import { addUnit, addStyle } from '../../libs/function/index';
/**
* Alert 警告提示
* @description 警告提示,展现需要关注的信息。
* @tutorial https://ijry.github.io/uview-plus/components/alertTips.html
*
* @property {String} title 显示的文字
* @property {String} type 使用预设的颜色 (默认 'warning'
* @property {String} description 辅助性文字颜色比title浅一点字号也小一点可选
* @property {Boolean} closable 关闭按钮(默认为叉号icon图标) (默认 false
* @property {Boolean} showIcon 是否显示左边的辅助图标 默认 false
* @property {String} effect 多图时,图片缩放裁剪的模式 (默认 'light'
* @property {Boolean} center 文字是否居中 (默认 false
* @property {String | Number} fontSize 字体大小 (默认 14
* @property {Object} customStyle 定义需要用到的外部样式
* @event {Function} click 点击组件时触发
* @event {Function} close 点击关闭按钮时触发
* @example <u-alert :title="title" type = "warning" :closable="closable" :description = "description"></u-alert>
*/
export default {
name: 'u-alert',
mixins: [mpMixin, mixin, props],
data() {
return {
show: true
}
},
computed: {
iconColor() {
return this.effect === 'light' ? this.type : '#fff'
},
// 不同主题对应不同的图标
iconName() {
switch (this.type) {
case 'success':
return 'checkmark-circle-fill';
break;
case 'error':
return 'close-circle-fill';
break;
case 'warning':
return 'error-circle-fill';
break;
case 'info':
return 'info-circle-fill';
break;
case 'primary':
return 'more-circle-fill';
break;
default:
return 'error-circle-fill';
}
}
},
emits: ["click","close"],
methods: {
addUnit,
addStyle,
// 点击内容
clickHandler() {
this.$emit('click')
},
// 点击关闭按钮
closeHandler() {
this.show = false
this.$emit('close')
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-alert {
position: relative;
background-color: $u-primary;
padding: 8px 10px;
@include flex(row);
align-items: center;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
&--primary--dark {
background-color: $u-primary;
}
&--primary--light {
background-color: #ecf5ff;
}
&--error--dark {
background-color: $u-error;
}
&--error--light {
background-color: #FEF0F0;
}
&--success--dark {
background-color: $u-success;
}
&--success--light {
background-color: #f5fff0;
}
&--warning--dark {
background-color: $u-warning;
}
&--warning--light {
background-color: #FDF6EC;
}
&--info--dark {
background-color: $u-info;
}
&--info--light {
background-color: #f4f4f5;
}
&__icon {
margin-right: 5px;
}
&__content {
@include flex(column);
flex: 1;
&__title {
color: $u-main-color;
font-size: 14px;
font-weight: bold;
color: #fff;
margin-bottom: 2px;
}
&__desc {
color: $u-main-color;
font-size: 14px;
flex-wrap: wrap;
color: #fff;
}
}
&__title--dark,
&__desc--dark {
color: #FFFFFF;
}
&__text--primary--light,
&__text--primary--light {
color: $u-primary;
}
&__text--success--light,
&__text--success--light {
color: $u-success;
}
&__text--warning--light,
&__text--warning--light {
color: $u-warning;
}
&__text--error--light,
&__text--error--light {
color: $u-error;
}
&__text--info--light,
&__text--info--light {
color: $u-info;
}
&__close {
position: absolute;
top: 11px;
right: 10px;
}
}
</style>

View File

@@ -0,0 +1,23 @@
/*
* @Author : LQ
* @Description :
* @version : 1.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:49:55
* @FilePath : /u-view2.0/uview-ui/libs/config/props/avatarGroup.js
*/
export default {
// avatarGroup 组件
avatarGroup: {
urls: [],
maxCount: 5,
shape: 'circle',
mode: 'scaleToFill',
showMore: true,
size: 40,
keyName: '',
gap: 0.5,
extraValue: 0
}
}

View File

@@ -0,0 +1,54 @@
import { defineMixin } from '../../libs/vue'
import defProps from '../../libs/config/props.js'
export const props = defineMixin({
props: {
// 头像图片组
urls: {
type: Array,
default: () => defProps.avatarGroup.urls
},
// 最多展示的头像数量
maxCount: {
type: [String, Number],
default: () => defProps.avatarGroup.maxCount
},
// 头像形状
shape: {
type: String,
default: () => defProps.avatarGroup.shape
},
// 图片裁剪模式
mode: {
type: String,
default: () => defProps.avatarGroup.mode
},
// 超出maxCount时是否显示查看更多的提示
showMore: {
type: Boolean,
default: () => defProps.avatarGroup.showMore
},
// 头像大小
size: {
type: [String, Number],
default: () => defProps.avatarGroup.size
},
// 指定从数组的对象元素中读取哪个属性作为图片地址
keyName: {
type: String,
default: () => defProps.avatarGroup.keyName
},
// 头像之间的遮挡比例
gap: {
type: [String, Number],
validator(value) {
return value >= 0 && value <= 1
},
default: () => defProps.avatarGroup.gap
},
// 需额外显示的值
extraValue: {
type: [Number, String],
default: () => defProps.avatarGroup.extraValue
}
}
})

View File

@@ -0,0 +1,110 @@
<template>
<view class="u-avatar-group">
<view
class="u-avatar-group__item"
v-for="(item, index) in showUrl"
:key="index"
:style="{
marginLeft: index === 0 ? 0 : addUnit(-size * gap)
}"
>
<u-avatar
:size="size"
:shape="shape"
:mode="mode"
:src="testObject(item) ? keyName && item[keyName] || item.url : item"
></u-avatar>
<view
class="u-avatar-group__item__show-more"
v-if="showMore && index === showUrl.length - 1 && (urls.length > maxCount || extraValue > 0)"
@tap="clickHandler"
>
<up-text
color="#ffffff"
:size="size * 0.4"
:text="`+${extraValue || urls.length - showUrl.length}`"
align="center"
customStyle="justify-content: center"
></up-text>
</view>
</view>
</view>
</template>
<script>
import { props } from './props';
import { mpMixin } from '../../libs/mixin/mpMixin';
import { mixin } from '../../libs/mixin/mixin';
import { addUnit } from '../../libs/function/index';
import test from '../../libs/function/test';
/**
* AvatarGroup 头像组
* @description 本组件一般用于展示头像的地方,如个人中心,或者评论列表页的用户头像展示等场所。
* @tutorial https://ijry.github.io/uview-plus/components/avatar.html
*
* @property {Array} urls 头像图片组 (默认 []
* @property {String | Number} maxCount 最多展示的头像数量 默认 5
* @property {String} shape 头像形状( 'circle' (默认) | 'square'
* @property {String} mode 图片裁剪模式(默认 'scaleToFill'
* @property {Boolean} showMore 超出maxCount时是否显示查看更多的提示 (默认 true
* @property {String | Number} size 头像大小 (默认 40
* @property {String} keyName 指定从数组的对象元素中读取哪个属性作为图片地址
* @property {String | Number} gap 头像之间的遮挡比例0.4代表遮挡40% (默认 0.5
* @property {String | Number} extraValue 需额外显示的值
* @event {Function} showMore 头像组更多点击
* @example <u-avatar-group:urls="urls" size="35" gap="0.4" ></u-avatar-group:urls=>
*/
export default {
name: 'u-avatar-group',
mixins: [mpMixin, mixin, props],
data() {
return {
}
},
computed: {
showUrl() {
return this.urls.slice(0, this.maxCount)
}
},
emits: ["showMore"],
methods: {
addUnit,
testObject: test.object,
clickHandler() {
this.$emit('showMore')
}
},
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-avatar-group {
@include flex;
&__item {
margin-left: -10px;
position: relative;
&--no-indent {
// 如果你想质疑作者不会使用:first-child说明你太年轻因为nvue不支持
margin-left: 0;
}
&__show-more {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.3);
@include flex;
align-items: center;
justify-content: center;
border-radius: 100px;
}
}
}
</style>

View File

@@ -0,0 +1,28 @@
/*
* @Author : LQ
* @Description :
* @version : 1.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:49:22
* @FilePath : /u-view2.0/uview-ui/libs/config/props/avatar.js
*/
export default {
// avatar 组件
avatar: {
src: '',
shape: 'circle',
size: 40,
mode: 'scaleToFill',
text: '',
bgColor: '#c0c4cc',
color: '#ffffff',
fontSize: 18,
icon: '',
mpAvatar: false,
randomBgColor: false,
defaultUrl: '',
colorIndex: '',
name: ''
}
}

View File

@@ -0,0 +1,81 @@
import { defineMixin } from '../../libs/vue'
import defProps from '../../libs/config/props.js'
import test from '../../libs/function/test';
export const props = defineMixin({
props: {
// 头像图片路径(不能为相对路径)
src: {
type: String,
default: () => defProps.avatar.src
},
// 头像形状circle-圆形square-方形
shape: {
type: String,
default: () => defProps.avatar.shape
},
// 头像尺寸
size: {
type: [String, Number],
default: () => defProps.avatar.size
},
// 裁剪模式
mode: {
type: String,
default: () => defProps.avatar.mode
},
// 显示的文字
text: {
type: String,
default: () => defProps.avatar.text
},
// 背景色
bgColor: {
type: String,
default: () => defProps.avatar.bgColor
},
// 文字颜色
color: {
type: String,
default: () => defProps.avatar.color
},
// 文字大小
fontSize: {
type: [String, Number],
default: () => defProps.avatar.fontSize
},
// 显示的图标
icon: {
type: String,
default: () => defProps.avatar.icon
},
// 显示小程序头像只对百度微信QQ小程序有效
mpAvatar: {
type: Boolean,
default: () => defProps.avatar.mpAvatar
},
// 是否使用随机背景色
randomBgColor: {
type: Boolean,
default: () => defProps.avatar.randomBgColor
},
// 加载失败的默认头像(组件有内置默认图片)
defaultUrl: {
type: String,
default: () => defProps.avatar.defaultUrl
},
// 如果配置了randomBgColor为true且配置了此值则从默认的背景色数组中取出对应索引的颜色值取值0-19之间
colorIndex: {
type: [String, Number],
// 校验参数规则索引在0-19之间
validator(n) {
return test.range(n, [0, 19]) || n === ''
},
default: () => defProps.avatar.colorIndex
},
// 组件标识符
name: {
type: String,
default: () => defProps.avatar.name
}
}
})

View File

@@ -0,0 +1,180 @@
<template>
<view
class="u-avatar"
:class="[`u-avatar--${shape}`]"
:style="[{
backgroundColor: (text || icon) ? (randomBgColor ? colors[colorIndex !== '' ? colorIndex : random(0, 19)] : bgColor) : 'transparent',
width: addUnit(size),
height: addUnit(size),
}, addStyle(customStyle)]"
@tap="clickHandler"
>
<slot>
<!-- #ifdef MP-WEIXIN || MP-QQ || MP-BAIDU -->
<open-data
v-if="mpAvatar && allowMp"
type="userAvatarUrl"
:style="[{
width: addUnit(size),
height: addUnit(size)
}]"
/>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN && MP-QQ && MP-BAIDU -->
<template v-if="mpAvatar && allowMp"></template>
<!-- #endif -->
<u-icon
v-else-if="icon"
:name="icon"
:size="fontSize"
:color="color"
></u-icon>
<up-text
v-else-if="text"
:text="text"
:size="fontSize"
:color="color"
align="center"
customStyle="justify-content: center"
></up-text>
<image
class="u-avatar__image"
v-else
:class="[`u-avatar__image--${shape}`]"
:src="avatarUrl || defaultUrl"
:mode="mode"
@error="errorHandler"
:style="[{
width: addUnit(size),
height: addUnit(size)
}]"
></image>
</slot>
</view>
</template>
<script>
import { props } from './props';
import { mpMixin } from '../../libs/mixin/mpMixin';
import { mixin } from '../../libs/mixin/mixin';
import { addStyle, addUnit, random } from '../../libs/function/index';
const base64Avatar =
"data:image/jpg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAA8AAD/4QMraHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjMtYzAxMSA2Ni4xNDU2NjEsIDIwMTIvMDIvMDYtMTQ6NTY6MjcgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDUzYgKFdpbmRvd3MpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjREMEQwRkY0RjgwNDExRUE5OTY2RDgxODY3NkJFODMxIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjREMEQwRkY1RjgwNDExRUE5OTY2RDgxODY3NkJFODMxIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NEQwRDBGRjJGODA0MTFFQTk5NjZEODE4Njc2QkU4MzEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NEQwRDBGRjNGODA0MTFFQTk5NjZEODE4Njc2QkU4MzEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7/7gAOQWRvYmUAZMAAAAAB/9sAhAAGBAQEBQQGBQUGCQYFBgkLCAYGCAsMCgoLCgoMEAwMDAwMDBAMDg8QDw4MExMUFBMTHBsbGxwfHx8fHx8fHx8fAQcHBw0MDRgQEBgaFREVGh8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx//wAARCADIAMgDAREAAhEBAxEB/8QAcQABAQEAAwEBAAAAAAAAAAAAAAUEAQMGAgcBAQAAAAAAAAAAAAAAAAAAAAAQAAIBAwICBgkDBQAAAAAAAAABAhEDBCEFMVFBYXGREiKBscHRMkJSEyOh4XLxYjNDFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A/fAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHbHFyZ/Dam+yLA+Z2L0Pjtyj2poD4AAAAAAAAAAAAAAAAAAAAAAAAKWFs9y6lcvvwQeqj8z9wFaziY1n/HbUX9XF97A7QAGXI23EvJ1goyfzR0YEfN269jeZ+a03pNe0DIAAAAAAAAAAAAAAAAAAAACvtO3RcVkXlWutuL9YFYAAAAAOJRjKLjJVi9GmB5/csH/mu1h/in8PU+QGMAAAAAAAAAAAAAAAAAAaMDG/6MmMH8C80+xAelSSVFolwQAAAAAAAHVlWI37ErUulaPk+hgeYnCUJuElSUXRrrQHAAAAAAAAAAAAAAAAABa2Oz4bM7r4zdF2ICmAAAAAAAAAg7zZ8GX41wuJP0rRgYAAAAAAAAAAAAAAAAAD0m2R8ODaXU33tsDSAAAAAAAAAlb9HyWZcnJd9PcBHAAAAAAAAAAAAAAAAAPS7e64Vn+KA0AAAAAAAAAJm+v8Ftf3ewCKAAAAAAAAAAAAAAAAAX9muqeGo9NttP06+0DcAAAAAAAAAjb7dTu2ra+VOT9P8AQCWAAAAAAAAAAAAAAAAAUNmyPt5Ltv4bui/kuAF0AAAAAAADiUlGLlJ0SVW+oDzOXfd/Ind6JPRdS0QHSAAAAAAAAAAAAAAAAAE2nVaNcGB6Lbs6OTao9LsF51z60BrAAAAAABJ3jOVHjW3r/sa9QEgAAAAAAAAAAAAAAAAAAAPu1duWriuW34ZR4MC9hbnZyEoy8l36XwfYBsAAADaSq9EuLAlZ+7xSdrGdW9Hc5dgEdtt1erfFgAAAAAAAAAAAAAAAAADVjbblX6NR8MH80tEBRs7HYivyzlN8lovaBPzduvY0m6eK10TXtAyAarO55lpJK54orolr+4GqO/Xaea1FvqbXvA+Z77kNeW3GPbV+4DJfzcm/pcm3H6Vou5AdAFLC2ed2Pjv1txa8sV8T6wOL+yZEKu1JXFy4MDBOE4ScZxcZLinoB8gAAAAAAAAAAAB242LeyJ+C3GvN9C7QLmJtePYpKS+5c+p8F2IDYAANJqj1T4oCfk7Nj3G5Wn9qXJax7gJ93Z82D8sVNc4v30A6Xg5i42Z+iLfqARwcyT0sz9MWvWBps7LlTf5Grce9/oBTxdtxseklHxT+uWr9AGoAB138ezfj4bsFJdD6V2MCPm7RdtJzs1uW1xXzL3gTgAAAAAAAAADRhYc8q74I6RWs5ckB6GxYtWLat21SK731sDsAAAAAAAAAAAAAAAASt021NO/YjrxuQXT1oCOAAAAAAABzGLlJRSq26JAelwsWONYjbXxcZvmwO8AAAAAAAAAAAAAAAAAAef3TEWPkVivx3NY9T6UBiAAAAAABo2+VmGXblddIJ8eivRUD0oAAAAAAAAAAAAAAAAAAAYt4tKeFKVNYNSXfRgefAAAAAAAAr7VuSSWPedKaW5v1MCsAAAAAAAAAAAAAAAAAAIe6bj96Ts2n+JPzSXzP3ATgAAAAAAAAFbbt1UUrOQ9FpC4/UwK6aaqtU+DAAAAAAAAAAAAAAA4lKMIuUmoxWrb4ARNx3R3q2rLpa4Sl0y/YCcAAAAAAAAAAANmFud7G8r89r6X0dgFvGzLGRGtuWvTF6NAdwAAAAAAAAAAAy5W442PVN+K59EePp5ARMvOv5MvO6QXCC4AZwAAAAAAAAAAAAAcxlKLUotprg1owN+PvORborq+7Hnwl3gUbO74VzRydt8pKn68ANcJwmqwkpLmnUDkAAAAfNy9atqtyagut0AxXt5xIV8Fbj6lRd7Am5G65V6qUvtwfyx94GMAAAAAAAAAAAAAAAAAAAOU2nVOj5gdsc3LiqRvTpyqwOxbnnrhdfpSfrQB7pnv/AGvuS9gHXPMy5/Fem1yq0v0A6W29XqwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf//Z";
/**
* Avatar 头像
* @description 本组件一般用于展示头像的地方,如个人中心,或者评论列表页的用户头像展示等场所。
* @tutorial https://ijry.github.io/uview-plus/components/avatar.html
*
* @property {String} src 头像路径,如加载失败,将会显示默认头像(不能为相对路径)
* @property {String} shape 头像形状 circle (默认) | square
* @property {String | Number} size 头像尺寸,可以为指定字符串(large, default, mini),或者数值 (默认 40
* @property {String} mode 头像图片的裁剪类型与uni的image组件的mode参数一致如效果达不到需求可尝试传widthFix值 (默认 'scaleToFill'
* @property {String} text 用文字替代图片级别优先于src
* @property {String} bgColor 背景颜色,一般显示文字时用 (默认 '#c0c4cc'
* @property {String} color 文字颜色 (默认 '#ffffff'
* @property {String | Number} fontSize 文字大小 (默认 18
* @property {String} icon 显示的图标
* @property {Boolean} mpAvatar 显示小程序头像只对百度微信QQ小程序有效 (默认 false
* @property {Boolean} randomBgColor 是否使用随机背景色 (默认 false
* @property {String} defaultUrl 加载失败的默认头像(组件有内置默认图片)
* @property {String | Number} colorIndex 如果配置了randomBgColor为true且配置了此值则从默认的背景色数组中取出对应索引的颜色值取值0-19之间
* @property {String} name 组件标识符 (默认 'level'
* @property {Object} customStyle 定义需要用到的外部样式
*
* @event {Function} click 点击组件时触发 index: 用户传递的标识符
* @example <u-avatar :src="src" mode="square"></u-avatar>
*/
export default {
name: 'u-avatar',
mixins: [mpMixin, mixin, props],
data() {
return {
// 如果配置randomBgColor参数为true在图标或者文字的模式下会随机从中取出一个颜色值当做背景色
colors: ['#ffb34b', '#f2bba9', '#f7a196', '#f18080', '#88a867', '#bfbf39', '#89c152', '#94d554', '#f19ec2',
'#afaae4', '#e1b0df', '#c38cc1', '#72dcdc', '#9acdcb', '#77b1cc', '#448aca', '#86cefa', '#98d1ee',
'#73d1f1',
'#80a7dc'
],
avatarUrl: this.src,
allowMp: false
}
},
watch: {
// 监听头像src的变化赋值给内部的avatarUrl变量因为图片加载失败时需要修改图片的src为默认值
// 而组件内部不能直接修改props的值所以需要一个中间变量
src: {
immediate: true,
handler(newVal) {
this.avatarUrl = newVal
// 如果没有传src则主动触发error事件用于显示默认的头像否则src为''空字符等的时候,会无内容展示
if(!newVal) {
this.errorHandler()
}
}
}
},
computed: {
imageStyle() {
const style = {}
return style
}
},
created() {
this.init()
},
emits: ["click"],
methods: {
addStyle,
addUnit,
random,
init() {
// 目前只有这几个小程序平台具有open-data标签
// 其他平台可以通过uni.getUserInfo类似接口获取信息但是需要弹窗授权(首次),不合符组件逻辑
// 故目前自动获取小程序头像只支持这几个平台
// #ifdef MP-WEIXIN || MP-QQ || MP-BAIDU
this.allowMp = true
// #endif
},
// 判断传入的name属性是否图片路径只要带有"/"均认为是图片形式
isImg() {
return this.src.indexOf('/') !== -1
},
// 图片加载时失败时触发
errorHandler() {
this.avatarUrl = this.defaultUrl || base64Avatar
},
clickHandler(e) {
this.$emit('click', this.name, e)
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-avatar {
@include flex;
align-items: center;
justify-content: center;
&--circle {
border-radius: 100px;
}
&--square {
border-radius: 4px;
}
&__image {
&--circle {
border-radius: 100px;
overflow: hidden;
}
&--square {
border-radius: 4px;
}
}
}
</style>

View File

@@ -0,0 +1,27 @@
/*
* @Author : LQ
* @Description :
* @version : 1.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : LQ
* @lastTime : 2021-08-20 16:50:18
* @FilePath : /u-view2.0/uview-ui/libs/config/props/backtop.js
*/
export default {
// backtop组件
backtop: {
mode: 'circle',
icon: 'arrow-upward',
text: '',
duration: 100,
scrollTop: 0,
top: 400,
bottom: 100,
right: 20,
zIndex: 9,
iconStyle: {
color: '#909399',
fontSize: '19px'
}
}
}

View File

@@ -0,0 +1,56 @@
import { defineMixin } from '../../libs/vue'
import defProps from '../../libs/config/props.js'
export const props = defineMixin({
props: {
// 返回顶部的形状circle-圆形square-方形
mode: {
type: String,
default: () => defProps.backtop.mode
},
// 自定义图标
icon: {
type: String,
default: () => defProps.backtop.icon
},
// 提示文字
text: {
type: String,
default: () => defProps.backtop.text
},
// 返回顶部滚动时间
duration: {
type: [String, Number],
default: () => defProps.backtop.duration
},
// 滚动距离
scrollTop: {
type: [String, Number],
default: () => defProps.backtop.scrollTop
},
// 距离顶部多少距离显示单位px
top: {
type: [String, Number],
default: () => defProps.backtop.top
},
// 返回顶部按钮到底部的距离单位px
bottom: {
type: [String, Number],
default: () => defProps.backtop.bottom
},
// 返回顶部按钮到右边的距离单位px
right: {
type: [String, Number],
default: () => defProps.backtop.right
},
// 层级
zIndex: {
type: [String, Number],
default: () => defProps.backtop.zIndex
},
// 图标的样式,对象形式
iconStyle: {
type: Object,
default: () => defProps.backtop.iconStyle
}
}
})

View File

@@ -0,0 +1,133 @@
<template>
<u-transition
mode="fade"
:customStyle="backTopStyle"
:show="show"
>
<view
class="u-back-top"
:style="[contentStyle]"
v-if="!$slots.default && !$slots.$default"
@click="backToTop"
>
<u-icon
:name="icon"
:custom-style="iconStyle"
></u-icon>
<text
v-if="text"
class="u-back-top__text"
>{{text}}</text>
</view>
<slot v-else />
</u-transition>
</template>
<script>
import { props } from './props';
import { mpMixin } from '../../libs/mixin/mpMixin';
import { mixin } from '../../libs/mixin/mixin';
import { addUnit, addStyle, getPx, deepMerge, error } from '../../libs/function/index';
// #ifdef APP-NVUE
const dom = weex.requireModule('dom')
// #endif
/**
* backTop 返回顶部
* @description 本组件一个用于长页面,滑动一定距离后,出现返回顶部按钮,方便快速返回顶部的场景。
* @tutorial https://uview-plus.jiangruyi.com/components/backTop.html
*
* @property {String} mode 返回顶部的形状circle-圆形square-方形 (默认 'circle'
* @property {String} icon 自定义图标 (默认 'arrow-upward' 见官方文档示例
* @property {String} text 提示文字
* @property {String | Number} duration 返回顶部滚动时间 (默认 100
* @property {String | Number} scrollTop 滚动距离 (默认 0
* @property {String | Number} top 距离顶部多少距离显示单位px (默认 400
* @property {String | Number} bottom 返回顶部按钮到底部的距离单位px (默认 100
* @property {String | Number} right 返回顶部按钮到右边的距离单位px (默认 20
* @property {String | Number} zIndex 层级 (默认 9
* @property {Object<Object>} iconStyle 图标的样式,对象形式 (默认 {color: '#909399',fontSize: '19px'}
* @property {Object} customStyle 定义需要用到的外部样式
*
* @example <u-back-top :scrollTop="scrollTop"></u-back-top>
*/
export default {
name: 'u-back-top',
mixins: [mpMixin, mixin, props],
computed: {
backTopStyle() {
// 动画组件样式
const style = {
bottom: addUnit(this.bottom),
right: addUnit(this.right),
width: '40px',
height: '40px',
position: 'fixed',
zIndex: 10,
}
return style
},
show() {
return getPx(this.scrollTop) > getPx(this.top)
},
contentStyle() {
const style = {}
let radius = 0
// 是否圆形
if(this.mode === 'circle') {
radius = '100px'
} else {
radius = '4px'
}
// 为了兼容安卓nvue只能这么分开写
style.borderTopLeftRadius = radius
style.borderTopRightRadius = radius
style.borderBottomLeftRadius = radius
style.borderBottomRightRadius = radius
return deepMerge(style, addStyle(this.customStyle))
}
},
emits: ["click"],
methods: {
backToTop() {
// #ifdef APP-NVUE
if (!this.$parent.$refs['u-back-top']) {
error(`nvue页面需要给页面最外层元素设置"ref='u-back-top'`)
}
dom.scrollToElement(this.$parent.$refs['u-back-top'], {
offset: 0
})
// #endif
// #ifndef APP-NVUE
uni.pageScrollTo({
scrollTop: 0,
duration: this.duration
});
// #endif
this.$emit('click')
}
}
}
</script>
<style lang="scss" scoped>
@import '../../libs/css/components.scss';
$u-back-top-flex:1 !default;
$u-back-top-height:100% !default;
$u-back-top-background-color:#E1E1E1 !default;
$u-back-top-tips-font-size:12px !default;
.u-back-top {
@include flex;
flex-direction: column;
align-items: center;
flex:$u-back-top-flex;
height: $u-back-top-height;
justify-content: center;
background-color: $u-back-top-background-color;
&__tips {
font-size:$u-back-top-tips-font-size;
transform: scale(0.8);
}
}
</style>

View File

@@ -0,0 +1,27 @@
/*
* @Author : LQ
* @Description :
* @version : 1.0
* @Date : 2021-08-20 16:44:21
* @LastAuthor : LQ
* @lastTime : 2021-08-23 19:51:50
* @FilePath : /u-view2.0/uview-ui/libs/config/props/badge.js
*/
export default {
// 徽标数组件
badge: {
isDot: false,
value: '',
show: true,
max: 999,
type: 'error',
showZero: false,
bgColor: null,
color: null,
shape: 'circle',
numberType: 'overflow',
offset: [],
inverted: false,
absolute: false
}
}

View File

@@ -0,0 +1,79 @@
import { defineMixin } from '../../libs/vue'
import defProps from '../../libs/config/props.js'
export const props = defineMixin({
props: {
// 是否显示圆点
isDot: {
type: Boolean,
default: () => defProps.badge.isDot
},
// 显示的内容
value: {
type: [Number, String],
default: () => defProps.badge.value
},
// 显示的内容
modelValue: {
type: [Number, String],
default: () => defProps.badge.modelValue
},
// 是否显示
show: {
type: Boolean,
default: () => defProps.badge.show
},
// 最大值,超过最大值会显示 '{max}+'
max: {
type: [Number, String],
default: () => defProps.badge.max
},
// 主题类型error|warning|success|primary
type: {
type: String,
default: () => defProps.badge.type
},
// 当数值为 0 时,是否展示 Badge
showZero: {
type: Boolean,
default: () => defProps.badge.showZero
},
// 背景颜色优先级比type高如设置type参数会失效
bgColor: {
type: [String, null],
default: () => defProps.badge.bgColor
},
// 字体颜色
color: {
type: [String, null],
default: () => defProps.badge.color
},
// 徽标形状circle-四角均为圆角horn-左下角为直角
shape: {
type: String,
default: () => defProps.badge.shape
},
// 设置数字的显示方式overflow|ellipsis|limit
// overflow会根据max字段判断超出显示`${max}+`
// ellipsis会根据max判断超出显示`${max}...`
// limit会依据1000作为判断条件超出1000显示`${value/1000}K`比如2.2k、3.34w最多保留2位小数
numberType: {
type: String,
default: () => defProps.badge.numberType
},
// 设置badge的位置偏移格式为 [x, y]也即设置的为top和right的值absolute为true时有效
offset: {
type: Array,
default: () => defProps.badge.offset
},
// 是否反转背景和字体颜色
inverted: {
type: Boolean,
default: () => defProps.badge.inverted
},
// 是否绝对定位
absolute: {
type: Boolean,
default: () => defProps.badge.absolute
}
}
})

View File

@@ -0,0 +1,177 @@
<template>
<text
v-if="show && ((Number(value) === 0 ? showZero : true) || isDot)"
:class="[isDot ? 'u-badge--dot' : 'u-badge--not-dot', inverted && 'u-badge--inverted', shape === 'horn' && 'u-badge--horn', `u-badge--${type}${inverted ? '--inverted' : ''}`]"
:style="[addStyle(customStyle), badgeStyle]"
class="u-badge"
>{{ isDot ? '' :showValue }}</text>
</template>
<script>
import { props } from './props';
import { mpMixin } from '../../libs/mixin/mpMixin';
import { mixin } from '../../libs/mixin/mixin';
import { addStyle, addUnit } from '../../libs/function/index';
/**
* badge 徽标数
* @description 该组件一般用于图标右上角显示未读的消息数量,提示用户点击,有圆点和圆包含文字两种形式。
* @tutorial https://uview-plus.jiangruyi.com/components/badge.html
*
* @property {Boolean} isDot 是否显示圆点 (默认 false
* @property {String | Number} value 显示的内容
* @property {Boolean} show 是否显示 (默认 true
* @property {String | Number} max 最大值,超过最大值会显示 '{max}+' 默认999
* @property {String} type 主题类型error|warning|success|primary (默认 'error'
* @property {Boolean} showZero 当数值为 0 时,是否展示 Badge (默认 false
* @property {String} bgColor 背景颜色优先级比type高如设置type参数会失效
* @property {String} color 字体颜色 (默认 '#ffffff'
* @property {String} shape 徽标形状circle-四角均为圆角horn-左下角为直角 (默认 'circle'
* @property {String} numberType 设置数字的显示方式overflow|ellipsis|limit (默认 'overflow'
* @property {Array}} offset 设置badge的位置偏移格式为 [x, y]也即设置的为top和right的值absolute为true时有效
* @property {Boolean} inverted 是否反转背景和字体颜色(默认 false
* @property {Boolean} absolute 是否绝对定位(默认 false
* @property {Object} customStyle 定义需要用到的外部样式
* @example <u-badge :type="type" :count="count"></u-badge>
*/
export default {
name: 'u-badge',
mixins: [mpMixin, props, mixin],
computed: {
// 是否将badge中心与父组件右上角重合
boxStyle() {
let style = {};
return style;
},
// 整个组件的样式
badgeStyle() {
const style = {}
if(this.color) {
style.color = this.color
}
if (this.bgColor && !this.inverted) {
style.backgroundColor = this.bgColor
}
if (this.absolute) {
style.position = 'absolute'
// 如果有设置offset参数
if(this.offset.length) {
// top和right分为为offset的第一个和第二个值如果没有第二个值则right等于top
const top = this.offset[0]
const right = this.offset[1] || top
style.top = addUnit(top)
style.right = addUnit(right)
}
}
return style
},
showValue() {
switch (this.numberType) {
case "overflow":
return Number(this.value) > Number(this.max) ? this.max + "+" : this.value
break;
case "ellipsis":
return Number(this.value) > Number(this.max) ? "..." : this.value
break;
case "limit":
return Number(this.value) > 999 ? Number(this.value) >= 9999 ?
Math.floor(this.value / 1e4 * 100) / 100 + "w" : Math.floor(this.value /
1e3 * 100) / 100 + "k" : this.value
break;
default:
return Number(this.value)
}
},
},
methods: {
addStyle
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
$u-badge-primary: $u-primary !default;
$u-badge-error: $u-error !default;
$u-badge-success: $u-success !default;
$u-badge-info: $u-info !default;
$u-badge-warning: $u-warning !default;
$u-badge-dot-radius: 100px !default;
$u-badge-dot-size: 8px !default;
$u-badge-dot-right: 4px !default;
$u-badge-dot-top: 0 !default;
$u-badge-text-font-size: 11px !default;
$u-badge-text-right: 10px !default;
$u-badge-text-padding: 2px 5px !default;
$u-badge-text-align: center !default;
$u-badge-text-color: #FFFFFF !default;
.u-badge {
border-top-right-radius: $u-badge-dot-radius;
border-top-left-radius: $u-badge-dot-radius;
border-bottom-left-radius: $u-badge-dot-radius;
border-bottom-right-radius: $u-badge-dot-radius;
@include flex;
line-height: $u-badge-text-font-size;
text-align: $u-badge-text-align;
font-size: $u-badge-text-font-size;
color: $u-badge-text-color;
&--dot {
height: $u-badge-dot-size;
width: $u-badge-dot-size;
}
&--inverted {
font-size: 13px;
}
&--not-dot {
padding: $u-badge-text-padding;
}
&--horn {
border-bottom-left-radius: 0;
}
&--primary {
background-color: $u-badge-primary;
}
&--primary--inverted {
color: $u-badge-primary;
}
&--error {
background-color: $u-badge-error;
}
&--error--inverted {
color: $u-badge-error;
}
&--success {
background-color: $u-badge-success;
}
&--success--inverted {
color: $u-badge-success;
}
&--info {
background-color: $u-badge-info;
}
&--info--inverted {
color: $u-badge-info;
}
&--warning {
background-color: $u-badge-warning;
}
&--warning--inverted {
color: $u-badge-warning;
}
}
</style>

View File

@@ -0,0 +1,27 @@
import { defineMixin } from '../../libs/vue'
import defProps from '../../libs/config/props.js'
export const propsBox = defineMixin({
props: {
// 背景色
bgColors: {
type: [Array],
default: ['#EEFCFF', '#FCF8FF', '#FDF8F2']
},
// 高度
height: {
type: [String],
default: "160px"
},
// 圆角
borderRadius: {
type: [String],
default: "6px"
},
// 间隔
gap: {
type: [String],
default: "15px"
},
}
})

View File

@@ -0,0 +1,92 @@
<template>
<view class="u-box" :style="[{height: height}, addStyle(customStyle)]">
<view class="u-box__left" :style="{borderRadius: borderRadius, backgroundColor: bgColors[0]}">
<slot name="left"></slot>
</view>
<view class="u-box__gap" :style="{width: gap, height: height}"></view>
<view class="u-box__right">
<view class="u-box__right-top" :style="{borderRadius: borderRadius, backgroundColor: bgColors[1]}">
<slot name="rightTop">右上</slot>
</view>
<view class="u-box__right-gap" :style="{height: gap}"></view>
<view class="u-box__right-bottom" :style="{borderRadius: borderRadius, backgroundColor: bgColors[2]}">
<slot name="rightBottom">右下</slot>
</view>
</view>
</view>
</template>
<script>
import { propsBox } from './props';
import { mpMixin } from '../../libs/mixin/mpMixin';
import { mixin } from '../../libs/mixin/mixin';
import { addStyle } from '../../libs/function/index';
import test from '../../libs/function/test';
/**
* box 盒子
* @description box盒子一般为左边一个盒子右侧两个等高的半盒组成常用于App首页座位重点突出。
* @tutorial https://uview-plus.jiangruyi.com/components/box.html
* @property {Array} bgColors 背景色
* @property {String} height 高度
* @property {String} borderRadius 圆角
* @property {Object} customStyle 定义需要用到的外部样式
*
* @event {Function} click 点击cell列表时触发
* @example <up-box colors=['blue', 'red', 'yellow'] height="200px"></up-box>
*/
export default {
name: 'up-box',
data() {
return {
}
},
mixins: [mpMixin, mixin, propsBox],
computed: {
},
emits: [],
methods: {
addStyle,
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/components.scss";
.u-box {
/* #ifndef APP-NVUE */
/* #endif */
@include flex();
flex: 1;
&__left {
@include flex();
justify-content: center;
align-items: center;
flex: 1;
}
&__gap {
@include flex();
flex-direction: column;
}
&__right {
@include flex();
flex-direction: column;
flex: 1;
}
&__right-top {
@include flex();
flex: 1;
justify-content: center;
align-items: center;
}
&__right-bottom {
@include flex();
flex: 1;
justify-content: center;
align-items: center;
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More