优化商城
This commit is contained in:
parent
3637447ab6
commit
757289558f
@ -7,7 +7,8 @@ export default {
|
||||
backgroundTextStyle: 'light',
|
||||
navigationBarBackgroundColor: '#fff',
|
||||
navigationBarTitleText: 'WeChat',
|
||||
navigationBarTextStyle: 'black'
|
||||
navigationBarTextStyle: 'black',
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
tabBar: {
|
||||
list: [
|
||||
|
@ -4,14 +4,14 @@
|
||||
// export const BASE_URL = `http://10.0.0.5:50001/lymarket`
|
||||
// export const BASE_URL = `http://192.168.0.89:40001/lymarket`
|
||||
// export const BASE_URL = `http://192.168.1.165:40001/lymarket` // 王霞
|
||||
export const BASE_URL = `https://test.zzfzyc.com/lymarket` // 测试环境
|
||||
// export const BASE_URL = `https://test.zzfzyc.com/lymarket` // 测试环境
|
||||
// export const BASE_URL = `http://192.168.1.9:40001/lymarket` // 发
|
||||
// export const BASE_URL = `http://192.168.1.9:50005/lymarket` // 发
|
||||
// export const BASE_URL = `http://192.168.1.30:50001/lymarket` // 发
|
||||
// export const BASE_URL = `https://dev.zzfzyc.com/lymarket` // 开发环境
|
||||
// export const BASE_URL = `https://www.zzfzyc.com/lymarket` // 正式环境
|
||||
// export const BASE_URL = `http://192.168.1.5:40001/lymarket` // 王霞
|
||||
// export const BASE_URL = `http://192.168.1.7:50002/lymarket` // 添
|
||||
export const BASE_URL = `http://192.168.1.7:50002/lymarket` // 添
|
||||
// export const BASE_URL = `http://192.168.1.42:50001/lymarket` // 杰
|
||||
|
||||
// CDN
|
||||
|
@ -23,7 +23,15 @@ export default (props:params) => {
|
||||
|
||||
const getData = async () => {
|
||||
const res = await fetchData()
|
||||
setList(res.data.list)
|
||||
setList(res.data?.list)
|
||||
}
|
||||
|
||||
const skipTo = (item) => {
|
||||
if(item.jump_type == 2) {
|
||||
goLink(item.link + '&title=' + item.title)
|
||||
} else {
|
||||
goLink(item.link)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@ -38,7 +46,7 @@ export default (props:params) => {
|
||||
{
|
||||
list?.map(item => {
|
||||
return <SwiperItem key={item.id}>
|
||||
<View className={styles.image_item} onClick={() => goLink(item.link)}>
|
||||
<View className={styles.image_item} onClick={() => skipTo(item)}>
|
||||
<Image mode="aspectFill" src={item.prev_view_url}></Image>
|
||||
</View>
|
||||
</SwiperItem>
|
||||
|
@ -4,12 +4,17 @@ import { ReactElement, useEffect, useRef, useState } from "react"
|
||||
import classnames from "classnames";
|
||||
import styles from './index.module.scss'
|
||||
import { GetShoppingCartApi } from "@/api/shopCart";
|
||||
import useCommonData from "@/use/useCommonData";
|
||||
import { useSelector } from "@/reducers/hooks";
|
||||
|
||||
type param = {
|
||||
children?: ReactElement|null,
|
||||
onClick?: () => void
|
||||
}
|
||||
export default ({children = null, onClick}:param) => {
|
||||
//获取购物车数据数量
|
||||
const {getShopCount, commonData} = useCommonData()
|
||||
|
||||
const [screenHeight, setScreenHeight] = useState(0)
|
||||
const [showMoveBtn, setShowMoveBtn] = useState(false)
|
||||
const screenWidthRef = useRef(0)
|
||||
@ -23,17 +28,8 @@ export default ({children = null, onClick}:param) => {
|
||||
setShowMoveBtn(true)
|
||||
})
|
||||
|
||||
//获取数据
|
||||
const [list, setList] = useState<any[]>([])
|
||||
const {fetchData} = GetShoppingCartApi()
|
||||
const getShoppingCart = async () => {
|
||||
const {data} = await fetchData()
|
||||
let color_list = data.color_list||[]
|
||||
setList(color_list)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getShoppingCart()
|
||||
getShopCount()
|
||||
}, [])
|
||||
|
||||
const dragEnd = (e) => {
|
||||
@ -45,7 +41,7 @@ export default ({children = null, onClick}:param) => {
|
||||
{children}
|
||||
{showMoveBtn&&<MovableView onClick={onClick} className={styles.moveBtn} direction="all" inertia={true} x="630rpx" y={screenHeight+'rpx'} onTouchEnd={(e) => dragEnd(e)}>
|
||||
<View className={classnames('iconfont','icon-gouwuche', styles.shop_icon) } ></View>
|
||||
{(list.length > 0)&&<View className={styles.product_num}>{list.length > 99?'99+':list.length}</View>}
|
||||
{(commonData.shopCount > 0)&&<View className={styles.product_num}>{commonData.shopCount > 99?'99+':commonData.shopCount}</View>}
|
||||
</MovableView>}
|
||||
</MovableArea>
|
||||
)
|
||||
|
@ -13,7 +13,7 @@
|
||||
box-sizing: border-box;
|
||||
color: $color_font_two;
|
||||
.miconfont{
|
||||
font-size: 30px;
|
||||
font-size: 40px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import { setParam } from "@/common/system";
|
||||
import { debounce } from "@/common/util";
|
||||
import Counter from "../counter";
|
||||
import { ApplyOrderAccessApi, GetAdminUserInfoApi, SubscriptionMessageApi } from "@/api/user";
|
||||
import useCommonData from "@/use/useCommonData";
|
||||
|
||||
type param = {
|
||||
show?: true|false,
|
||||
@ -37,7 +38,8 @@ export default ({show = false, onClose}: param) => {
|
||||
setSelectStatus(true)
|
||||
}, [selectIndex])
|
||||
|
||||
|
||||
//获取购物车数据数量
|
||||
const {setShopCount} = useCommonData()
|
||||
|
||||
//重置勾选数据
|
||||
const resetList = () => {
|
||||
@ -58,6 +60,7 @@ export default ({show = false, onClose}: param) => {
|
||||
const getShoppingCart = async () => {
|
||||
const {data} = await fetchData()
|
||||
let color_list = data.color_list||[]
|
||||
setShopCount(color_list.length)
|
||||
initList(color_list)
|
||||
setList(color_list)
|
||||
setLoading(false)
|
||||
@ -143,6 +146,8 @@ export default ({show = false, onClose}: param) => {
|
||||
setShowPopup(false)
|
||||
}
|
||||
|
||||
|
||||
|
||||
//删除购物车内容
|
||||
const {fetchData:delShopFetchData} = DelShoppingCartApi()
|
||||
const delSelect = () => {
|
||||
@ -152,7 +157,6 @@ export default ({show = false, onClose}: param) => {
|
||||
content: '删除所选商品?',
|
||||
success: async function (res) {
|
||||
if (res.confirm) {
|
||||
|
||||
const res = await delShopFetchData({id:selectIds.current})
|
||||
if(res.success) {
|
||||
getShoppingCart()
|
||||
@ -160,6 +164,7 @@ export default ({show = false, onClose}: param) => {
|
||||
title: '成功',
|
||||
icon: 'success',
|
||||
})
|
||||
|
||||
} else {
|
||||
Taro.showToast({
|
||||
title: res.msg,
|
||||
@ -192,7 +197,6 @@ export default ({show = false, onClose}: param) => {
|
||||
|
||||
//格式化数量
|
||||
const formatCount = useCallback((item) => {
|
||||
console.log('item:::',item)
|
||||
return item.sale_mode == 0? item.roll : (item.length/100)
|
||||
}, [])
|
||||
|
||||
|
@ -1,2 +1,3 @@
|
||||
export const SET_SHOPCOUNT = 'set_shopCount'
|
||||
export const CLEAR_SHOPCOUNT = 'clear_shopCount'
|
||||
export const STORAGE_SHOPCOUNT = 'storage_shopcount'
|
@ -1,4 +1,4 @@
|
||||
export default {
|
||||
navigationBarTitleText: '我的收藏a',
|
||||
navigationBarTitleText: '收藏详情',
|
||||
enableShareAppMessage: true,
|
||||
}
|
||||
|
@ -26,6 +26,15 @@
|
||||
.operation_check{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
&::after{
|
||||
content: '';
|
||||
height: 45px;
|
||||
width: 1PX;
|
||||
background-color: #ccc;
|
||||
position: absolute;
|
||||
right: -30px;
|
||||
}
|
||||
Text{
|
||||
margin-left: 15px;
|
||||
height: 100%;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { CreateFavoriteApi, DelFavoriteApi, DelFavoriteProductApi, DetailFavoriteProductApi, FavoriteListApi, MoveFavoriteProductApi, UpdateFavoriteApi } from "@/api/favorite";
|
||||
import { DelFavoriteProductApi, DetailFavoriteProductApi, MoveFavoriteProductApi } from "@/api/favorite";
|
||||
import { alert } from "@/common/common";
|
||||
import { getFilterData } from "@/common/util";
|
||||
import Product from "../components/product";
|
||||
@ -7,8 +7,6 @@ import { Text, View } from "@tarojs/components"
|
||||
import Taro, { useRouter } from "@tarojs/taro";
|
||||
import classnames from "classnames";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import CreatePopup from "../components/createPopup";
|
||||
import UpdatePopup from "../components/updatePopup";
|
||||
import styles from './index.module.scss'
|
||||
import MCheckbox from "@/components/checkbox";
|
||||
import AddCollection from "@/components/addCollection";
|
||||
@ -24,6 +22,9 @@ export default () => {
|
||||
const getFavoriteInfo = async () => {
|
||||
let res = await fetchDataDetailFavoriteProduct(searchData)
|
||||
if(res.success) {
|
||||
Taro.setNavigationBarTitle({
|
||||
title: res.data.name
|
||||
})
|
||||
setColorInfo(res.data)
|
||||
}
|
||||
}
|
||||
@ -89,6 +90,7 @@ export default () => {
|
||||
//移动面料
|
||||
const {fetchData: fetchDataMoveFavoriteProduct} = MoveFavoriteProductApi()
|
||||
const onAdd = async () => {
|
||||
if(ids.length == 0) return alert.none('请选择要移动面料')
|
||||
let res = await fetchDataMoveFavoriteProduct({favorite_id: searchData.id, product_id:ids})
|
||||
if(res.success) {
|
||||
getFavoriteInfo()
|
||||
@ -115,7 +117,7 @@ export default () => {
|
||||
<View className={styles.operation}>
|
||||
<View className={styles.operation_check}>
|
||||
<MCheckbox status={allSelectStatus} onSelect={() => selectCallBack()} onClose={() => colseCallBack()}/>
|
||||
<Text>全选</Text>
|
||||
<Text className={styles.allSelect}>全选</Text>
|
||||
</View>
|
||||
<View className={styles.operation_check_right}>
|
||||
<Text className={styles.operation_check_move} onClick={() => setCollectionShow(true)}>移动到</Text>
|
||||
|
@ -17,6 +17,7 @@ import { formatHashTag, formatPriceDiv } from "@/common/fotmat";
|
||||
import { debounce, getFilterData } from "@/common/util";
|
||||
import LabAndImg from "@/components/LabAndImg";
|
||||
import VirtualList from '@tarojs/components/virtual-list'
|
||||
import useCommonData from "@/use/useCommonData";
|
||||
|
||||
|
||||
|
||||
@ -139,6 +140,7 @@ export default memo(({show = false, onClose, title = '', productId = 0}: param)
|
||||
}
|
||||
|
||||
//添加购物车
|
||||
const {getShopCount} = useCommonData()
|
||||
const {getSelfUserInfo} = UseLogin()
|
||||
const {fetchData:addFetchData} = AddShoppingCartApi()
|
||||
const addShopCart = async () => {
|
||||
@ -167,6 +169,7 @@ export default memo(({show = false, onClose, title = '', productId = 0}: param)
|
||||
Taro.showToast({
|
||||
title:'添加成功'
|
||||
})
|
||||
getShopCount()
|
||||
onClose?.()
|
||||
} else {
|
||||
Taro.showToast({
|
||||
|
@ -1,17 +1,17 @@
|
||||
|
||||
import { Button, CustomWrapper, Image, RichText, Text, View } from '@tarojs/components'
|
||||
import Taro, { useDidShow, usePullDownRefresh, useRouter, useShareAppMessage } from '@tarojs/taro';
|
||||
import { Button, CustomWrapper, RichText, Text, View } from '@tarojs/components'
|
||||
import Taro, { useDidShow, usePullDownRefresh, useRouter } from '@tarojs/taro';
|
||||
import classnames from "classnames";
|
||||
import DesSwiper from './components/swiper';
|
||||
import OrderCount from './components/orderCount';
|
||||
import ShopCart from '@/components/shopCart';
|
||||
import Preview,{colorItem} from './components/preview';
|
||||
import styles from './index.module.scss'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {formatHashTag, formatImgUrl} from '@/common/fotmat'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {formatHashTag} from '@/common/fotmat'
|
||||
import {GetProductDetailApi} from '@/api/material'
|
||||
import useLogin from '@/use/useLogin';
|
||||
import { AnalysisShortCodeApi, BindShortCodeApi, GetShortCodeApi } from '@/api/share';
|
||||
import { AnalysisShortCodeApi, GetShortCodeApi } from '@/api/share';
|
||||
import { SHARE_SCENE } from '@/common/enum';
|
||||
import useUserInfo from '@/use/useUserInfo';
|
||||
import LabAndImg from '@/components/LabAndImg';
|
||||
@ -19,6 +19,8 @@ import { alert } from '@/common/common';
|
||||
import AddCollection from '@/components/addCollection';
|
||||
import { AddFavoriteApi, DelFavoriteProductApi } from '@/api/favorite';
|
||||
import { GetShoppingCartApi } from '@/api/shopCart';
|
||||
import { useSelector } from '@/reducers/hooks';
|
||||
import useCommonData from '@/use/useCommonData';
|
||||
|
||||
type item = {title:string, img:string, url:string, id:number}
|
||||
|
||||
@ -50,10 +52,13 @@ export default (props:Params) => {
|
||||
setParams({id: res.data.product_id, share: res.data})
|
||||
}
|
||||
|
||||
//获取购物车数据数量
|
||||
const {getShopCount, commonData} = useCommonData()
|
||||
|
||||
useDidShow(() => {
|
||||
judgeParam()
|
||||
setShowCart(false)
|
||||
getShoppingCart()
|
||||
getShopCount()
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
@ -170,14 +175,8 @@ export default (props:Params) => {
|
||||
}
|
||||
}
|
||||
|
||||
//获取购物车数据数量
|
||||
const [shopCount, setShopCount] = useState<number>(0)
|
||||
const {fetchData: fetchDataShopCount} = GetShoppingCartApi()
|
||||
const getShoppingCart = async () => {
|
||||
const {data} = await fetchDataShopCount()
|
||||
let color_list = data.color_list||[]
|
||||
setShopCount(color_list.length)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//页面下拉刷新
|
||||
@ -233,7 +232,7 @@ export default (props:Params) => {
|
||||
<View className={styles.buy_cart} onClick={() => setShowCart(true)}>
|
||||
<View className={classnames('iconfont icon-gouwuche', styles.miconfont)}></View>
|
||||
<View className={styles.text}>购物车</View>
|
||||
{(shopCount > 0)&&<View className={styles.product_num}>{shopCount > 99?'99+':shopCount}</View>}
|
||||
{(commonData.shopCount > 0)&&<View className={styles.product_num}>{commonData.shopCount > 99?'99+':commonData.shopCount}</View>}
|
||||
</View>
|
||||
{
|
||||
(!userInfo.adminUserInfo?.is_authorize_phone)&&<View className={styles.buy_btn} >
|
||||
|
@ -46,7 +46,6 @@ export default () => {
|
||||
}
|
||||
//监听查询条件
|
||||
useEffect(() => {
|
||||
|
||||
if (filtrate.product_kind_id)
|
||||
getProductList()
|
||||
}, [filtrate])
|
||||
@ -94,7 +93,7 @@ export default () => {
|
||||
<View className={styles.main}>
|
||||
<Banner />
|
||||
<View className={styles.search}>
|
||||
<View className={styles.search_collect}>我的收藏</View>
|
||||
<View className={styles.search_collect} onClick={() => goLink('/pages/collection/index')}>我的收藏</View>
|
||||
<View className={styles.search_input} onClick={() => goLink('/pages/searchList/search')}>
|
||||
<Search disabled={true} style={{ width: '263rpx' }} />
|
||||
</View>
|
||||
|
@ -113,17 +113,6 @@ import styles from './index.module.scss'
|
||||
const onShowLogistics = useCallback((val) => {
|
||||
setLogisticsShow(true)
|
||||
if(val != 1) setLogistics(true)
|
||||
// if(val == 1) {
|
||||
// setLogisticsShow(true)
|
||||
// } else {
|
||||
// const list = orderDetail?.accessory_url.map(item => {
|
||||
// return formatImgUrl(item)
|
||||
// })
|
||||
// Taro.previewImage({
|
||||
// current: list[0], // 当前显示
|
||||
// urls: list // 需要预览的图片http链接列表
|
||||
// })
|
||||
// }
|
||||
}, [])
|
||||
const onCloseLogistics = useCallback(() => {
|
||||
setLogisticsShow(false)
|
||||
|
@ -1,3 +1,3 @@
|
||||
export default {
|
||||
navigationBarTitleText: '分类标题'
|
||||
navigationBarTitleText: '专题页面'
|
||||
}
|
||||
|
@ -12,11 +12,19 @@ import {GetProductListApi} from '@/api/material'
|
||||
import { useRouter } from "@tarojs/taro";
|
||||
import { dataLoadingStatus, getFilterData } from "@/common/util";
|
||||
import LoadingCard from "@/components/loadingCard";
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
export default () => {
|
||||
const [showPopup, setShowPopup] = useState(false)
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
Taro.setNavigationBarTitle({
|
||||
title: router.params.title||'专题页面'
|
||||
})
|
||||
}, [router])
|
||||
|
||||
//搜索参数
|
||||
const [searchField, setSearchField] = useState({
|
||||
code_or_name: '',
|
||||
@ -47,7 +55,6 @@ export default () => {
|
||||
|
||||
//上拉加载数据
|
||||
const pageNum = useRef({size: searchField.size, page: searchField.page})
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const getScrolltolower = () => {
|
||||
if(subjectList.list.length < subjectList.total) {
|
||||
pageNum.current.page++
|
||||
|
@ -2,11 +2,12 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import {
|
||||
SET_SHOPCOUNT,
|
||||
CLEAR_SHOPCOUNT
|
||||
CLEAR_SHOPCOUNT,
|
||||
STORAGE_SHOPCOUNT
|
||||
} from '../constants/common'
|
||||
|
||||
export type DataParam = {
|
||||
shopCount: number
|
||||
shopCount: number //购物车数量
|
||||
}
|
||||
|
||||
type Action = {
|
||||
@ -14,21 +15,19 @@ type Action = {
|
||||
data?: DataParam
|
||||
}
|
||||
|
||||
|
||||
|
||||
const INIT = {
|
||||
shopCount: Taro.getStorageSync('shopCount')?JSON.parse(Taro.getStorageSync('shopCount')):null,
|
||||
shopCount: Taro.getStorageSync(STORAGE_SHOPCOUNT)?JSON.parse(Taro.getStorageSync(STORAGE_SHOPCOUNT)).shopCount:0,
|
||||
}
|
||||
|
||||
export default function counter (state = INIT, action: Action) {
|
||||
const {type, data} = action
|
||||
switch (type) {
|
||||
case SET_SHOPCOUNT:
|
||||
Taro.setStorageSync('shopCount',JSON.stringify(data))
|
||||
Taro.setStorageSync(STORAGE_SHOPCOUNT,JSON.stringify(data?.shopCount))
|
||||
return {...state,...data}
|
||||
case CLEAR_SHOPCOUNT:
|
||||
Taro.removeStorageSync('shopCount')
|
||||
return {...state, shopCount: null}
|
||||
Taro.removeStorageSync(STORAGE_SHOPCOUNT)
|
||||
return {...state, shopCount: 0}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
|
@ -1,51 +1,33 @@
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import {SET_SHOPCOUNT, CLEAR_SHOPCOUNT} from '@/constants/common'
|
||||
import {DataParam} from '@/reducers/commonData'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { GetShoppingCartApi } from '@/api/shopCart'
|
||||
import { useSelector } from '@/reducers/hooks'
|
||||
export default () => {
|
||||
const commonObj = useSelector((state:DataParam) => state) as DataParam
|
||||
const commonData = useSelector(state => state.commonData)
|
||||
const dispatch = useDispatch()
|
||||
|
||||
// const setToken = (shopCount: number) => {
|
||||
// dispatch({type:SET_SHOPCOUNT, data:{shopCount}})
|
||||
// }
|
||||
|
||||
// const setSessionKey = (sessionkey: string) => {
|
||||
// dispatch({type:SET_SESSIONKEY, data:{session_key: sessionkey}})
|
||||
// }
|
||||
|
||||
// const setUserInfo = (userInfo: UserParam) => {
|
||||
// dispatch({type:SET_USERINFO, data:{userInfo}})
|
||||
// }
|
||||
|
||||
// const setAdminUserInfo = (adminUserInfo: UserAdminParam) => {
|
||||
// dispatch({type:SET_ADMINUSERINFO, data:{adminUserInfo}})
|
||||
// }
|
||||
|
||||
// const setSortCode = (sortCode:SortCodeParam) => {
|
||||
// dispatch({type:SET_SORTCODE, data:{sort_code:sortCode}})
|
||||
// }
|
||||
|
||||
// const removeUserInfo = () => {
|
||||
// dispatch({type:CLEAR_USERINFO})
|
||||
// }
|
||||
|
||||
// const removeToken = () => {
|
||||
// dispatch({type:CLEAR_TOKEN})
|
||||
// }
|
||||
|
||||
// const removeSessionKey = () => {
|
||||
// dispatch({type:CLEAR_SESSIONKEY})
|
||||
// }
|
||||
|
||||
// return {
|
||||
// setToken,
|
||||
// setUserInfo,
|
||||
// setAdminUserInfo,
|
||||
// setSessionKey,
|
||||
// removeUserInfo,
|
||||
// removeToken,
|
||||
// removeSessionKey,
|
||||
// setSortCode,
|
||||
// userInfo, //响应式数据返回
|
||||
// }
|
||||
const setShopCount = (shopCount: number) => {
|
||||
dispatch({type:SET_SHOPCOUNT, data:{shopCount}})
|
||||
}
|
||||
|
||||
const removeShopCount = () => {
|
||||
dispatch({type:CLEAR_SHOPCOUNT})
|
||||
}
|
||||
|
||||
const {fetchData: fetchDataShopCount} = GetShoppingCartApi()
|
||||
const getShopCount = async () => {
|
||||
//获取购物车数据数量
|
||||
const {data} = await fetchDataShopCount()
|
||||
let color_list = data.color_list||[]
|
||||
setShopCount(color_list.length)
|
||||
}
|
||||
|
||||
return {
|
||||
setShopCount,
|
||||
removeShopCount,
|
||||
getShopCount,
|
||||
commonData
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user