Compare commits

...

11 Commits
main ... csm

Author SHA1 Message Date
cf792b854a 富文本接口请求 2025-08-28 20:24:46 +08:00
45c0f1e05b 智慧关税 2025-08-28 18:55:24 +08:00
13b27ce641 海关直通车 2025-08-28 18:25:09 +08:00
635e28bb4b 首页、tab栏设置 2025-08-28 17:36:13 +08:00
6c969df959 文件名修改 2025-08-28 15:16:48 +08:00
860ce21b52 删除冗余代码 2025-08-28 14:54:05 +08:00
fe61729cb4 整理shop分包 2025-08-28 14:47:28 +08:00
6bf2a1ada9 商城分包相关内容 2025-08-28 11:58:40 +08:00
65c88c24eb 账户状态\待办事项\客服\客户管理页面迁移 2025-08-28 11:37:15 +08:00
0d17baa9c9 企业画像:贸易数据&出口数据 2025-08-28 10:19:14 +08:00
25341584fa 个人中心页面 2025-08-28 09:55:40 +08:00
499 changed files with 68363 additions and 1392 deletions

16
.hbuilderx/launch.json Normal file
View File

@ -0,0 +1,16 @@
{ // launch.json configurations app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
// launchtypelocalremote, localremote
"version": "0.0",
"configurations": [{
"default" :
{
"launchtype" : "local"
},
"mp-weixin" :
{
"launchtype" : "local"
},
"type" : "uniCloud"
}
]
}

View File

@ -62,6 +62,11 @@ https:://apis.haibao.shop
[腾讯的] (https://www.tapd.cn/tapd_fe/55592674/storywall)
```
```shell
12. 分包命名
注意:分包命名不能与主包相同,主包为`pages`,分包为`pagesXxx`xxx为分包名称例如商城相关分包命名为`pagesShop`
```
```shell
12. PC端首页 https://www.haibao.shop
13. 后台地址 https://apis.haibao.shop/index.html#/admin

976
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -59,11 +59,14 @@
"@dcloudio/uni-mp-xhs": "3.0.0-alpha-3081220230802001",
"@dcloudio/uni-quickapp-webview": "3.0.0-alpha-3081220230802001",
"@dcloudio/uni-ui": "^1.4.28",
"@tailwindcss/cli": "^4.1.12",
"lodash": "^4.17.21",
"pinia": "2.0.27",
"pinia-plugin-persistedstate": "^3.2.0",
"tailwindcss": "^4.1.12",
"vue": "^3.2.47",
"vue-i18n": "^9.2.2"
"vue-i18n": "^9.2.2",
"wot-design-uni": "^1.12.3"
},
"devDependencies": {
"@dcloudio/types": "^3.3.3",

715
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

28
project.config.json Normal file
View File

@ -0,0 +1,28 @@
{
"appid": "wxfd7d85cf2619b249",
"compileType": "miniprogram",
"libVersion": "3.9.2",
"packOptions": {
"ignore": [],
"include": []
},
"setting": {
"coverView": true,
"es6": true,
"postcss": true,
"minified": true,
"enhance": true,
"showShadowRootInWxmlPanel": true,
"packNpmRelationList": [],
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
}
},
"condition": {},
"editorSetting": {
"tabIndent": "insertSpaces",
"tabSize": 4
}
}

View File

@ -0,0 +1,7 @@
{
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
"projectname": "hbsmUniapp",
"setting": {
"compileHotReLoad": true
}
}

View File

@ -10,7 +10,7 @@ import { request } from '@/utils/request'
export const postAddressApi = (data: addressParams) => {
return request<addressItem>({
method: 'POST',
url: '/userAddress/add',
url: 'shop/userAddress/add',
data,
})
}
@ -18,7 +18,7 @@ export const postAddressApi = (data: addressParams) => {
export const getAddressApi = (data?: PageParams) => {
return request<PageResult<addressItem>>({
method: 'GET',
url: '/userAddress',
url: 'shop/userAddress',
data,
})
}
@ -26,7 +26,7 @@ export const getAddressApi = (data?: PageParams) => {
export const getAddressByIdApi = (id: string) => {
return request<addressItem>({
method: 'GET',
url: `/userAddress/detail`,
url: `shop/userAddress/detail`,
data: {
id,
},
@ -36,7 +36,7 @@ export const getAddressByIdApi = (id: string) => {
export const putAddressApi = (data: addressParams) => {
return request<addressItem>({
method: 'POST',
url: `/userAddress/edit`,
url: `shop/userAddress/edit`,
data,
})
}
@ -44,7 +44,7 @@ export const putAddressApi = (data: addressParams) => {
export const deleteAddressApi = (id: string | number) => {
return request({
method: 'POST',
url: `/userAddress/delete`,
url: `shop/userAddress/delete`,
data: {
id,
},

View File

@ -6,7 +6,7 @@ import { request } from '@/utils/request'
export const getSettingApi = () => {
return request<settingItem>({
method: 'GET',
url: '/shop/common/setting',
url: 'shop/common/setting',
})
}
@ -20,7 +20,7 @@ export const uploadApi = (filePath: string): Promise<AnyObject> => {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: `/shop/common/upload?driver=${uploadSetting.mode}`,
url: `shop/common/upload?driver=${uploadSetting.mode}`,
filePath,
name: 'file',
success: (res) => {
@ -55,7 +55,7 @@ export const uploadApi = (filePath: string): Promise<AnyObject> => {
export const getCaptchaData = (id: string) => {
return request({
url: '/api/common/clickCaptcha',
url: 'api/common/clickCaptcha',
method: 'GET',
data: {
id,
@ -65,7 +65,7 @@ export const getCaptchaData = (id: string) => {
export const getCaptchaCodeApi = (id: string) => {
return request({
url: '/api/common/captcha',
url: 'api/common/captcha',
method: 'GET',
data: {
id,
@ -76,7 +76,7 @@ export const getCaptchaCodeApi = (id: string) => {
export const checkClickCaptcha = (id: string, info: string, unset: boolean) => {
return request({
url: '/api/common/checkClickCaptcha',
url: 'api/common/checkClickCaptcha',
method: 'POST',
data: {
id,

37
src/api/customs.ts Normal file
View File

@ -0,0 +1,37 @@
import type { PageParams } from '@/types/global'
import { request } from '@/utils/request'
export const postConfigApi = (data?: PageParams) => {
return request(
{
method: 'POST',
url: 'customs/CustomsConfig/config',
data,
apiName:true
},
false,
)
}
export const postDataMainApi = (data?: PageParams) => {
return request({
method: 'POST',
url: 'customs/Index/dataMain',
data,
apiName:false
},
false,
)
}
export const postDataSmartApi = (data) => {
return request({
method: 'POST',
url: 'customsServices/CustomsServicesConfig/config',
data,
apiName:true
},
false,
)
}

View File

@ -11,7 +11,7 @@ import { request } from '@/utils/request'
export const getGoodsListApi = (data?: PageParams & goodsListParams) => {
return request<PageResult<goodsListItem>>({
method: 'GET',
url: '/shop/goods',
url: 'shop/goods',
data,
})
}
@ -19,7 +19,7 @@ export const getGoodsListApi = (data?: PageParams & goodsListParams) => {
export const getGoodsByIdApi = (id: number) => {
return request<goodsResult>({
method: 'GET',
url: '/shop/goods/detail',
url: 'shop/goods/detail',
data: {
id,
},
@ -29,7 +29,7 @@ export const getGoodsByIdApi = (id: number) => {
export const getGoodsServiceApi = (service_ids: string) => {
return request<goodsServiceItem[]>({
method: 'GET',
url: '/shop/goods/service',
url: 'shop/goods/service',
data: {
service_ids,
},
@ -39,7 +39,7 @@ export const getGoodsServiceApi = (service_ids: string) => {
export const getShareInfoApi = (id: number) => {
return request<goodsShareResult>({
method: 'GET',
url: '/shop/goods/share',
url: 'shop/goods/share',
data: {
id,
},

View File

@ -6,10 +6,10 @@ import { request } from '@/utils/request'
*
* @returns
*/
export const getBannerApi = (type: number = 1) => {
export const getBannerApi = (type : number = 1) => {
return request<bannerItem[]>({
method: 'GET',
url: '/shop/advert/banner',
url: 'shop/advert/banner',
data: {
type,
},
@ -21,10 +21,10 @@ export const getBannerApi = (type: number = 1) => {
* @param data
* @returns
*/
export const getGoodsListApi = (data?: PageParams) => {
export const getGoodsListApi = (data ?: PageParams) => {
return request<PageResult<goodsListItem>>({
method: 'GET',
url: '/shop/goods',
url: 'shop/goods',
data,
})
}
@ -37,13 +37,29 @@ export const getGoodsListApi = (data?: PageParams) => {
export const getNoticeBartApi = () => {
return request<noticeBarItem[]>({
method: 'GET',
url: '/shop/advert/noticeBar',
url: 'shop/advert/noticeBar',
})
}
export const getHotRecommendApi = () => {
return request<hotRecommendItem[]>({
method: 'GET',
url: '/shop/advert/hotRecommend',
url: 'shop/advert/hotRecommend',
})
}
// 移动端-首页接口
export const configIndex = (data : { page : number, page_size : number }) => {
return request({
url: 'api/index/config',
method: 'POST',
data,
})
}
// 移动端-首页接口-搜索接口
export const configSearch = (data:Record<string, any>) => {
return request({
url: 'api/index/search',
method: 'POST',
data,
})
}

10
src/api/other.ts Normal file
View File

@ -0,0 +1,10 @@
import { request } from '@/utils/request'
// 富文本案例介绍
export const editorApi = (data : { key : string }) => {
return request({
method: 'POST',
url: 'api/index/editor',
data,
})
}

47
src/api/portrait.ts Normal file
View File

@ -0,0 +1,47 @@
import type { PageParams } from '@/types/global'
import type { RootName } from '@/types/userCenter'
import { request } from '@/utils/request'
// 企业画像
export const postCompanyDataThirdDataViewApi = (data : PageParams) => {
return request<RootName>({
method: 'POST',
url: 'company/CompanyDataThird/dataView',
})
}
// 出口数据
export const postDataViewExportApi = (data : PageParams) => {
return request<RootName>({
method: 'POST',
url: 'company/CompanyDataThird/dataViewExport',
})
}
// 进口数据
export const postDataViewImportApi = (data : PageParams) => {
return request<RootName>({
method: 'POST',
url: 'company/CompanyDataThird/dataViewImport',
})
}
// 贸易伙伴
export const postDataViewPartnersApi = (data : PageParams) => {
return request<RootName>({
method: 'POST',
url: 'company/CompanyDataThird/dataViewPartners',
})
}
// 海关编码统计
export const postDataViewHscodeApi = (data : PageParams) => {
return request<RootName>({
method: 'POST',
url: 'company/CompanyDataThird/dataViewHscode',
})
}
// 海贸区域
export const postDataViewAreaApi = (data : PageParams) => {
return request<RootName>({
method: 'POST',
url: 'company/CompanyDataThird/dataViewArea',
})
}

18
src/api/userCenter.ts Normal file
View File

@ -0,0 +1,18 @@
import type { PageParams } from '@/types/global'
import type { RootName } from '@/types/userCenter'
import { request } from '@/utils/request'
// 个人中心按钮素材配置
export const postCenterConfigApi = (data:PageParams) => {
return request<RootName>({
method: 'POST',
url: 'shop/user/centerConfig',
})
}
// 个人中心数据
export const postUserInfoApi = (data:PageParams) => {
return request<RootName>({
method: 'POST',
url: 'shop/user/userInfo',
})
}

View File

@ -0,0 +1,113 @@
<template>
<view class="notice-bar">
<wd-notice-bar
:text="displayTexts"
color="#333"
background-color="#fff"
@click="handleNoticeClick"
>
<template #prefix>
<view class="notice-bar-left flex-row align-center">
<view class="notice-icon" v-if="props.prefix">
<wd-icon
class="prefix"
:name="props.noticeData.notice_img"
size="28rpx"
v-if="props.noticeData.notice_img"
></wd-icon>
</view>
<view class="header-text flex-row align-center">
<text class="smart-text floor-font-28">{{ props.firstText }}</text>
<text class="announcement-text floor-font-28">{{ props.secondText }}</text>
</view>
</view>
</template>
<template #suffix>
<view class="arrow-right" v-if="props.suffix">
<wd-icon name="arrow-right" size="24rpx"></wd-icon>
</view>
</template>
</wd-notice-bar>
</view>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const props = withDefaults(
defineProps<{
prefix?: boolean
suffix?: boolean
firstText?: string
secondText?: string
noticeData: {
notice_img?: string
list: Array<{
title: string
content: string
url: string
}>
}
}>(),
{
prefix: false,
suffix: false,
firstText: '智慧',
secondText: '公告'
}
)
const emits = defineEmits<{
(e: 'noticeClick', index: number): void
}>()
// content
const displayTexts = computed(() => {
// noticeData.listcontent
return props.noticeData?.list?.map(item => item.content) || []
})
const handleNoticeClick = ({ index }: { index: number }) => {
if (props.noticeData?.list?.[index]?.url) {
uni.navigateTo({
url: props.noticeData.list[index].url,
fail: () => {
uni.reLaunch({
url: props.noticeData.list[index].url
})
}
})
}
emits('noticeClick', index)
}
</script>
<style lang="scss" scoped>
@import '@/styles/common.scss';
// 穿
:deep(.wd-notice-bar__content) {
font-size: 24rpx;
color: #666; //
}
.notice-bar {
.header-text {
flex-wrap: wrap;
font-size: 28rpx;
margin-left: 4rpx;
.smart-text {
color: $else-highlight-color;
font-family: $font-family-bold;
font-weight: 700;
}
.announcement-text {
color: #333;
font-family: $font-family-bold;
font-weight: 700;
}
}
}
</style>

View File

@ -0,0 +1,183 @@
<template>
<view class="search-bar">
<view class="search-input-wrapper">
<view class="search-icon flex-row align-center">
<image src="/static/images/search.png" mode="aspectFit" class="search-icon-img"></image>
</view>
<input
class="search-input"
v-model="localSearchKeyword"
:placeholder="placeholder"
placeholder-class="placeholder"
@input="handleInput"
@confirm="handleSearch"
/>
<!-- 清除图标 -->
<view v-if="localSearchKeyword" class="clear-icon" @click="clearSearch">
<uni-icons type="clear" size="18" color="#999"></uni-icons>
</view>
</view>
<view v-if="showSearchButton" class="search-btn" @click="handleSearch">搜索</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { useSearch } from '@/composables/common/useSearch'
//
const props = defineProps({
placeholder: {
type: String,
default: '请输入搜索关键字',
},
showSearchButton: {
type: Boolean,
default: true,
},
showSearchText: {
type: Boolean,
default: true,
},
debounceTime: {
type: Number,
default: 300,
},
minLength: {
type: Number,
default: 1,
},
maxHistoryCount: {
type: Number,
default: 10,
},
})
const emit = defineEmits(['search', 'input', 'clear'])
//
const localSearchKeyword = ref('')
// 使
const {
isSearching,
searchHistory,
handleSearch: performSearch,
debounceSearch,
} = useSearch({
debounceTime: props.debounceTime,
minLength: props.minLength,
maxHistoryCount: props.maxHistoryCount,
})
//
const handleInput = (e) => {
const value = e.detail.value
emit('input', value)
//
debounceSearch(() => {
if (value.trim().length >= props.minLength) {
emit('search', value.trim())
}
})
}
//
const handleSearch = () => {
const keyword = localSearchKeyword.value.trim()
if (keyword.length >= props.minLength) {
performSearch(keyword)
emit('search', keyword)
}
}
//
const clearSearch = () => {
localSearchKeyword.value = ''
emit('clear')
}
//
defineExpose({
searchKeyword: localSearchKeyword,
isSearching,
searchHistory,
clearSearch,
handleSearch,
})
</script>
<style lang="scss" scoped>
@import '@/styles/common.scss';
.search-bar {
display: flex;
align-items: center;
width: 100%;
height: 80rpx;
padding: 0 30rpx;
background: #fff;
border-radius: 24rpx;
.search-input-wrapper {
flex: 1;
display: flex;
align-items: center;
margin-right: 24rpx;
position: relative; //
.search-icon {
margin-right: 16rpx;
font-size: 32rpx;
color: #999;
// background: red;
.search-icon-img {
width: 32rpx;
height: 32rpx;
}
}
.search-input {
flex: 1;
font-size: 28rpx;
color: #333;
border: none;
outline: none;
background: transparent;
padding-right: 40rpx; //
&::placeholder {
color: #999;
}
}
.clear-icon {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
padding: 8rpx;
cursor: pointer;
&:active {
opacity: 0.7;
}
}
}
.search-btn {
color: $secondary-color;
font-size: 28rpx;
font-weight: 500;
cursor: pointer;
&:active {
opacity: 0.7;
}
}
}
.placeholder {
color: #999;
}
</style>

View File

@ -0,0 +1,37 @@
<script setup lang="ts">
import { ref } from 'vue'
import '@/styles/common.scss'
const props = defineProps<{
title: string
color?: string
}>()
const emits = defineEmits<{
(event: 'titleClick'): void
}>()
const handleTitleClick = () => {
emits('titleClick')
}
</script>
<template>
<view class="section-title flex-row align-center floor-margin-top-40" @click="handleTitleClick">
<view class="title-indicator" :style="{ backgroundColor: color || '#4D7EEC' }"></view>
<text class="title-text floor-text-primary floor-text-bold floor-font-32">{{ title }}</text>
</view>
</template>
<style scoped lang="scss">
.section-title {
// margin: 0 0 30rpx 0;
.title-indicator {
width: 6rpx;
height: 31rpx;
border-radius: 3px;
margin-right: 8rpx;
}
}
</style>

View File

@ -0,0 +1,63 @@
import { computed } from 'vue'
export function useBackground() {
// 默认背景色
const defaultBackgroundColor = '#f1f1f1'
// 渐变色起始色
const gradientStartColor = '#C8E3FF'
// 渐变色结束色
const gradientEndColor = '#E8F4FF'
// 多端背景样式
const backgroundStyle = computed(() => {
// 默认样式
return {
background: defaultBackgroundColor,
minHeight: '100vh',
position: 'relative',
}
})
// 顶部渐变背景样式
const topGradientStyle = computed(() => ({
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '470rpx',
background: getGradientBackground(),
zIndex: 1,
}))
const setTopGradientStyle = (startColor: string, endColor: string) => ({
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '470rpx',
background: setGradientBackground(startColor, endColor),
zIndex: 1,
})
// 获取背景色值
const getBackgroundColor = () => defaultBackgroundColor
// 获取渐变背景色值
const getGradientBackground = () =>
`linear-gradient(180deg,
#C8E3FF 0%,
#E3ECF6 50%,
#f1f1f1 100%)`
//设置渐变背景色
const setGradientBackground = (startColor: string, endColor: string) =>
`linear-gradient(180deg,
${startColor} 0%,
${endColor} 50%,
#f1f1f1 100%)`
return {
backgroundStyle,
topGradientStyle,
getBackgroundColor,
getGradientBackground,
setTopGradientStyle
}
}

View File

@ -0,0 +1,124 @@
import { ref, computed, type Ref } from 'vue'
export interface FetchOptions<T = any> {
// 是否立即执行
immediate?: boolean
// 是否显示加载状态
showLoading?: boolean
// 加载提示文字
loadingText?: string
// 是否显示错误提示
showError?: boolean
// 错误提示文字
errorText?: string
// 重试次数
retryCount?: number
// 重试延迟
retryDelay?: number
}
export interface FetchResult<T = any> {
data: Ref<T | null>
loading: Ref<boolean>
error: Ref<string | null>
execute: (...args: any[]) => Promise<T>
refresh: () => Promise<T>
reset: () => void
}
export function useDataFetch<T = any>(
fetchFn: (...args: any[]) => Promise<T>,
options: FetchOptions<T> = {},
): FetchResult<T> {
const {
immediate = false,
showLoading = true,
loadingText = '加载中...',
showError = true,
errorText = '加载失败',
retryCount = 0,
retryDelay = 1000,
} = options
const data = ref<T | null>(null) as Ref<T | null>
const loading = ref(false)
const error = ref<string | null>(null)
const retryTimes = ref(0)
// 是否正在重试
const isRetrying = computed(() => retryTimes.value > 0)
// 执行数据获取
const execute = async (...args: any[]): Promise<T> => {
if (loading.value) return data.value as T
loading.value = true
error.value = null
if (showLoading) {
uni.showLoading({
title: loadingText,
mask: true,
})
}
try {
const result = await fetchFn(...args)
data.value = result
retryTimes.value = 0
return result
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err)
error.value = errorMessage
if (showError) {
uni.showToast({
title: errorText,
icon: 'error',
duration: 2000,
})
}
// 重试逻辑
if (retryTimes.value < retryCount) {
retryTimes.value++
await new Promise((resolve) => setTimeout(resolve, retryDelay))
return execute(...args)
}
throw err
} finally {
loading.value = false
if (showLoading) {
uni.hideLoading()
}
}
}
// 刷新数据
const refresh = async (): Promise<T> => {
return execute()
}
// 重置状态
const reset = () => {
data.value = null
loading.value = false
error.value = null
retryTimes.value = 0
}
// 立即执行
if (immediate) {
execute()
}
return {
data,
loading,
error,
execute,
refresh,
reset,
}
}

View File

@ -0,0 +1,113 @@
import { ref, computed, nextTick } from 'vue'
export interface RefreshLoadOptions {
// 刷新成功提示
showRefreshSuccess?: boolean
// 加载失败提示
showLoadError?: boolean
}
export function useRefreshLoad(options: RefreshLoadOptions = {}) {
const { showRefreshSuccess = true, showLoadError = true } = options
// 响应式状态
const isRefreshing = ref(false)
const isLoading = ref(false)
const hasMore = ref(true)
const currentPage = ref(1)
// 下拉刷新处理
const handleRefresh = async (refreshCallback?: () => Promise<void>) => {
if (isRefreshing.value) return
isRefreshing.value = true
currentPage.value = 1
hasMore.value = true
try {
if (refreshCallback) {
await refreshCallback()
}
if (showRefreshSuccess) {
uni.showToast({
title: '刷新成功',
icon: 'success',
duration: 1500,
})
}
} catch (error) {
console.error('刷新失败:', error)
if (showLoadError) {
uni.showToast({
title: '刷新失败',
icon: 'error',
duration: 1500,
})
}
} finally {
isRefreshing.value = false
// 停止下拉刷新动画
uni.stopPullDownRefresh()
}
}
// 上拉加载处理
const handleLoadMore = async (loadMoreCallback?: () => Promise<void>) => {
if (isLoading.value || !hasMore.value) return
isLoading.value = true
currentPage.value++
try {
if (loadMoreCallback) {
await loadMoreCallback()
}
} catch (error) {
console.error('加载失败:', error)
currentPage.value-- // 回退页码
if (showLoadError) {
uni.showToast({
title: '加载失败',
icon: 'error',
duration: 1500,
})
}
} finally {
isLoading.value = false
}
}
// 重置状态
const resetState = () => {
isRefreshing.value = false
isLoading.value = false
hasMore.value = true
currentPage.value = 1
}
// 设置加载状态
const setLoadingState = (loading: boolean) => {
isLoading.value = loading
}
// 设置是否有更多数据
const setHasMore = (hasMoreData: boolean) => {
hasMore.value = hasMoreData
}
return {
// 状态
isRefreshing,
isLoading,
hasMore,
currentPage,
// 方法
handleRefresh,
handleLoadMore,
resetState,
setLoadingState,
setHasMore,
}
}

View File

@ -0,0 +1,142 @@
import { ref } from 'vue'
export interface SearchOptions {
debounceTime?: number
minLength?: number
maxHistoryCount?: number
}
export function useSearch(options: SearchOptions = {}) {
const { debounceTime = 300, minLength = 1, maxHistoryCount = 10 } = options
const isSearching = ref(false)
const searchHistory = ref<string[]>([])
const searchTimer = ref<ReturnType<typeof setTimeout> | null>(null)
// 搜索处理
const handleSearch = (keyword: string) => {
const searchText = keyword.trim()
if (searchText.length < minLength) {
uni.showToast({
title: `请输入至少${minLength}个字符`,
icon: 'none',
duration: 1500,
})
return
}
// 添加到搜索历史
addToHistory(searchText)
// 执行搜索
performSearch(searchText)
}
// 执行搜索
const performSearch = async (keyword: string) => {
isSearching.value = true
try {
// 这里可以调用搜索API
console.log('执行搜索:', keyword)
// 模拟搜索延迟
await new Promise((resolve) => setTimeout(resolve, 500))
// 搜索成功回调
// uni.showToast({
// title: '搜索完成',
// icon: 'success',
// duration: 1000,
// })
} catch (error) {
console.error('搜索失败:', error)
uni.showToast({
title: '搜索失败',
icon: 'error',
duration: 1500,
})
} finally {
isSearching.value = false
}
}
// 添加到搜索历史
const addToHistory = (keyword: string) => {
console.log('addToHistory', keyword)
if (!keyword.trim()) return
// 移除重复项
const index = searchHistory.value.indexOf(keyword)
if (index > -1) {
searchHistory.value.splice(index, 1)
}
// 添加到开头
searchHistory.value.unshift(keyword)
// 限制历史记录数量
if (searchHistory.value.length > maxHistoryCount) {
searchHistory.value = searchHistory.value.slice(0, maxHistoryCount)
}
// 保存到本地存储
saveSearchHistory()
}
// 清空搜索历史
const clearSearchHistory = () => {
searchHistory.value = []
saveSearchHistory()
}
// 保存搜索历史到本地
const saveSearchHistory = () => {
try {
uni.setStorageSync('searchHistory', searchHistory.value)
} catch (error) {
console.error('保存搜索历史失败:', error)
}
}
// 从本地加载搜索历史
const loadSearchHistory = () => {
try {
const history = uni.getStorageSync('searchHistory')
if (history && Array.isArray(history)) {
searchHistory.value = history
}
} catch (error) {
console.error('加载搜索历史失败:', error)
}
}
// 防抖搜索
const debounceSearch = (callback: () => void) => {
if (searchTimer.value) {
clearTimeout(searchTimer.value)
}
searchTimer.value = setTimeout(() => {
callback()
}, debounceTime)
}
// 初始化时加载搜索历史
loadSearchHistory()
return {
// 状态
isSearching,
searchHistory,
// 方法
handleSearch,
performSearch,
addToHistory,
clearSearchHistory,
debounceSearch,
loadSearchHistory,
}
}

View File

@ -0,0 +1,10 @@
import { useBackground } from './common/useBackground'
export function useCustomsExpress() {
const { backgroundStyle, topGradientStyle } = useBackground()
return {
backgroundStyle,
topGradientStyle,
}
}

View File

@ -0,0 +1,143 @@
import { ref } from 'vue'
import { useBackground } from './common/useBackground'
import { useRefreshLoad } from './common/useRefreshLoad'
import { useDataFetch } from './common/useDataFetch'
import { getBannerApi, getHotRecommendApi, getNoticeBartApi, configIndex } from '@/api/home'
import { getTopCategoryApi } from '@/api/catogory'
import type { bannerItem, noticeBarItem } from '@/types/home'
export function useHomePage() {
// ==================== 使用其他 composables ====================
const { backgroundStyle, topGradientStyle } = useBackground()
const {
isRefreshing,
isLoading,
hasMore,
currentPage,
handleRefresh,
handleLoadMore,
resetState,
} = useRefreshLoad({
showRefreshSuccess: true,
showLoadError: true,
})
const homeData = ref({})
// ==================== 页面数据 ====================
const bannerList = ref<bannerItem[]>([
{
id: 1,
image: '/static/images/home/banner1.png',
jump: true,
goods_id: '1',
},
{
id: 2,
image: '/static/images/home/banner1.png',
jump: true,
goods_id: '2',
},
{
id: 3,
image: '/static/images/home/banner1.png',
jump: true,
goods_id: '3',
},
])
const noticeBarList = ref<string[]>([
'公告公告公告公告公告1111111',
'智慧关务系统升级通知',
'保税物流服务优化公告',
'海关政策更新提醒',
'新功能上线通知',
])
const categoryList = ref<any[]>([])
const hotRecommendList = ref<any[]>([])
// ==================== 数据获取 ====================
const { execute: fetchHomeIndex } = useDataFetch(configIndex, {
immediate: false,
showLoading: false,
showError: false,
})
const { execute: fetchBanners } = useDataFetch(getBannerApi, {
immediate: false,
showLoading: false,
showError: false,
})
const { execute: fetchNotices } = useDataFetch(getNoticeBartApi, {
immediate: false,
showLoading: false,
showError: false,
})
const { execute: fetchCategories } = useDataFetch(getTopCategoryApi, {
immediate: false,
showLoading: false,
showError: false,
})
const { execute: fetchHotRecommend } = useDataFetch(getHotRecommendApi, {
immediate: false,
showLoading: false,
showError: false,
})
//页面初始化
const initPage = async () => {
try {
await Promise.all([fetchHomeIndex().then(res => {
if (res.code == 1) {
homeData.value = res.data
}
})])
// 在这里赋值或者获取接口返回的数据
} catch (error) {
console.error('页面初始化失败:', error)
}
}
// 刷新处理
const onRefresh = async () => {
await handleRefresh(async () => {
await initPage()
})
}
// 加载更多
const onLoadMore = async () => {
await handleLoadMore(async () => {
// 这里可以加载更多数据
console.log('加载更多数据')
})
}
// 返回数据和方法
return {
// 背景样式
backgroundStyle,
topGradientStyle,
// 页面状态
isRefreshing,
isLoading,
hasMore,
currentPage,
// 页面数据
bannerList,
noticeBarList,
categoryList,
hotRecommendList,
// 页面方法
initPage,
onRefresh,
onLoadMore,
resetState,
homeData
}
}

View File

@ -0,0 +1,293 @@
import { ref, unref, computed, nextTick } from 'vue'
import { useBackground } from './common/useBackground'
import { useRefreshLoad } from './common/useRefreshLoad'
import { useDataFetch } from './common/useDataFetch'
import { getBannerApi, getHotRecommendApi, getNoticeBartApi, configIndex, configSearch } from '@/api/home'
import { getTopCategoryApi } from '@/api/catogory'
import type { bannerItem, noticeBarItem } from '@/types/home'
export function useTodoPage() {
// ==================== 使用其他 composables ====================
const { backgroundStyle } = useBackground()
const topGradientStyle = useBackground().setTopGradientStyle('#FCDC9D', '#F2EFE7');
const {
isRefreshing,
isLoading,
hasMore,
currentPage,
handleRefresh,
handleLoadMore,
resetState,
} = useRefreshLoad({
showRefreshSuccess: true,
showLoadError: true,
})
// ==================== 页面数据 ====================
const bannerList = ref<bannerItem[]>([])
const noticeBarList = ref<string[]>([])
const categoryList = ref<any[]>([])
const hotRecommendList = ref<any[]>([])
// 新增接口返回的数据
const menuList = ref<any[]>([])
const switchList = ref<any[]>([])
const systemInfo = ref<any>({})
const noticeInfo = ref<any>({})
const hbsmNotice = ref<any>({})
const blackInfo = ref<any>({})
// ==================== Tab切换相关 ====================
const activeTab = ref(0) // 当前激活的tab索引
const activeTabKey = ref('goods') // 当前激活的tab key
const tabContentList = ref<any[]>([]) // tab对应的内容列表
const tabLoading = ref(false) // tab内容加载状态
const tabCurrentPage = ref(1) // tab内容当前页
const tabHasMore = ref(true) // tab内容是否还有更多
// ==================== 数据获取 ====================
const { execute: fetchBanners } = useDataFetch(getBannerApi, {
immediate: false,
showLoading: false,
showError: false,
})
const { execute: fetchNotices } = useDataFetch(getNoticeBartApi, {
immediate: false,
showLoading: false,
showError: false,
})
const { execute: fetchCategories } = useDataFetch(getTopCategoryApi, {
immediate: false,
showLoading: false,
showError: false,
})
const { execute: fetchHotRecommend } = useDataFetch(getHotRecommendApi, {
immediate: false,
showLoading: false,
showError: false,
})
// ==================== 页面初始化 ====================
const initPage = async () => {
try {
await Promise.all([
fetchBanners(),
fetchNotices(),
fetchCategories(),
fetchHotRecommend(),
configIndexApi()
])
// 初始化完成后加载默认tab的内容
if (switchList.value.length > 0) {
activeTabKey.value = switchList.value[0].keys
await loadTabContent(activeTabKey.value, 1, true)
}
} catch (error) {
console.error('页面初始化失败:', error)
}
}
// ==================== 刷新处理 ====================
const onRefresh = async () => {
await handleRefresh(async () => {
await initPage()
})
}
// ==================== 加载更多处理 ====================
const onLoadMore = async () => {
if (tabHasMore.value && !tabLoading.value) {
await loadTabContent(activeTabKey.value, tabCurrentPage.value + 1, false)
}
}
const configIndexApi = async () => {
try {
const res = await configIndex({ page: 1, page_size: 10 })
console.log(res, '-----------------configIndex')
if (res.code === 1 && res.data) {
const { data } = res
// 处理菜单数据
if (data.menu && Array.isArray(data.menu)) {
menuList.value = data.menu
}
bannerList.value = data.adv
// [
// { title: '推荐', route: '/pages/home/index' },
// { title: '合作伙伴', route: '/pagesMember/pages/profile/profile' },
// { title: '商机', route: '/pagesForeignTrade/pages/trade/trade' },
// { title: '关于我们', route: '/pagesOther/pages/blank/rich-text' },
// { title: '找买家', route: '/pagesMember/pages/profile/profile' },
// { title: '找卖家', route: '/pagesMember/pages/profile/profile' },
// ]
// 处理切换列表数据
if (data?.switchList && Array.isArray(data?.switchList)) {
switchList.value = data?.switchList
// 设置默认激活的tab
if (data.switchList.length > 0) {
activeTabKey.value = data.switchList[0].keys
}
}
// 处理系统信息
if (data.system) {
systemInfo.value = data.system
}
// 处理通知栏数据
if (data.notice) {
noticeInfo.value = data.notice
// 将通知列表转换为字符串数组格式
if (data.notice.list && Array.isArray(data.notice.list)) {
noticeBarList.value = data.notice.list.map((item : any) => item.content || item.title)
}
}
// 处理海保世贸通知
if (data.hbsmNotice) {
hbsmNotice.value = data.hbsmNotice;
//弹出系统维护窗口
//status 系统提示状态,1=弹出系统提示, 弹出hbsm_notice.title,和hbsm_notice.content ; 0不用管
if (data.hbsmNotice.status == 1)
uni.showModal({
showCancel: false,
title: data.hbsmNotice.title,
content: data.hbsmNotice.content
})
}
// 处理黑名单信息
if (data.black) {
blackInfo.value = data.black
//status 黑名单状态,1=已被拉黑,弹窗弹窗,不能关闭,或者跳到新页面无法返回
if (data.black.status == 1) {
uni.setStorage({
key: 'blackInfo',
data: data.black,
success: () => {
uni.reLaunch({
url: '/pages/black/index'
})
}
});
}
}
}
} catch (error) {
console.error('获取首页配置失败:', error)
}
}
// ==================== Tab切换和内容加载 ====================
/**
* Tab
* @param index tab索引
* @param tabKey tab的key值
*/
const switchTab = async (index : number, tabKey : string) => {
if (activeTab.value === index) return
activeTab.value = index
activeTabKey.value = tabKey
// 切换tab时重新加载内容
await loadTabContent(tabKey, 1, true)
}
/**
* Tab内容
* @param type tab类型
* @param page
* @param isRefresh
* @param keywords
*/
const loadTabContent = async (type : string, page : number = 1, isRefresh : boolean = false, keywords : string = '') => {
try {
tabLoading.value = true
const res = await configSearch({
page,
page_size: 10,
keywords,
type
})
console.log(res, '-----------------configSearch')
if (res.code === 1 && res.data) {
const { data } = res
if (isRefresh) {
// 刷新时清空原有数据
tabContentList.value = data.list || []
tabCurrentPage.value = 1
} else {
// 加载更多时追加数据
tabContentList.value = [...tabContentList.value, ...(data.list || [])]
tabCurrentPage.value = page
}
// 判断是否还有更多数据
tabHasMore.value = (data.list && data.list.length === 10) || false
}
} catch (error) {
console.error('加载tab内容失败:', error)
} finally {
tabLoading.value = false
}
}
// ==================== 返回数据和方法 ====================
return {
// 背景样式
backgroundStyle,
topGradientStyle,
// 页面状态
isRefreshing,
isLoading,
hasMore,
currentPage,
// 页面数据
bannerList,
noticeBarList,
categoryList,
hotRecommendList,
// 新增的接口数据
menuList,
switchList,
systemInfo,
noticeInfo,
hbsmNotice,
blackInfo,
// Tab相关数据和状态
activeTab,
activeTabKey,
tabContentList,
tabLoading,
tabCurrentPage,
tabHasMore,
// 页面方法
initPage,
onRefresh,
onLoadMore,
resetState,
// Tab相关方法
switchTab,
loadTabContent,
}
}

View File

@ -3,12 +3,12 @@
"autoscan": true,
"custom": {
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue",
"^shop-(.*)": "@/components/shop$1.vue"
"^shop-(.*)": "@/components/shop$1.vue",
"^wd-(.*)": "wot-design-uni/components/wd-$1/wd-$1.vue"
}
},
"pages": [
{
"path": "pages/index/index",
"pages": [{
"path": "pages/home/index",
"style": {
"navigationBarTitleText": "首页",
// #ifdef WEB
@ -17,29 +17,43 @@
}
},
{
"path": "pages/my/my",
"path": "pages/assistant/index",
"style": {
"navigationBarTextStyle": "white",
"navigationStyle": "custom",
"navigationBarTitleText": "我的"
"navigationBarTitleText": "海保助手"
}
},
{
"path": "pages/cart/cart",
"path": "pages/my/userCenter",
"style": {
"navigationBarTitleText": "购物车",
// #ifdef WEB
"navigationStyle": "custom"
// #endif
"navigationBarTextStyle": "white",
"navigationStyle": "custom",
"navigationBarTitleText": "个人中心"
}
},
{
"path": "pages/category/category",
"path": "pages/black/index",
"style": {
"navigationBarTitleText": "分类",
// #ifdef WEB
"navigationStyle": "custom"
// #endif
"navigationBarTextStyle": "white",
"navigationStyle": "custom",
"navigationBarTitleText": "账户状态"
}
},
{
"path": "pages/todo/index",
"style": {
"navigationBarTextStyle": "white",
"navigationStyle": "custom",
"navigationBarTitleText": "待办事项"
}
},
{
"path": "pages/service/index",
"style": {
"navigationBarTextStyle": "white",
"navigationStyle": "custom",
"navigationBarTitleText": "客服"
}
}
],
@ -51,81 +65,95 @@
},
"tabBar": {
"color": "#333",
"selectedColor": "#ff5f3c",
"fontSize": "20rpx",
"iconWidth": "36rpx",
"selectedColor": "#4d7eec",
"backgroundColor": "#fff",
"borderStyle": "white",
"list": [
{
"list": [{
"text": "首页",
"pagePath": "pages/index/index",
"iconPath": "/static/tabs/home_default.png",
"selectedIconPath": "/static/tabs/home_selected.png"
"pagePath": "pages/home/index",
"iconPath": "/static/tabs/home.png",
"selectedIconPath": "/static/tabs/home_active.png"
},
{
"text": "分类",
"pagePath": "pages/category/category",
"iconPath": "/static/tabs/category_default.png",
"selectedIconPath": "/static/tabs/category_selected.png"
"text": "海保助手",
"pagePath": "pages/assistant/index",
"iconPath": "/static/tabs/assistant.png",
"selectedIconPath": "/static/tabs/assistant_active.png"
},
{
"text": "购物车",
"pagePath": "pages/cart/cart",
"iconPath": "/static/tabs/cart_default.png",
"selectedIconPath": "/static/tabs/cart_selected.png"
"text": "待办事项",
"pagePath": "pages/todo/index",
"iconPath": "/static/tabs/todo.png",
"selectedIconPath": "/static/tabs/todo_active.png"
},
{
"text": "我的",
"pagePath": "pages/my/my",
"iconPath": "/static/tabs/user_default.png",
"selectedIconPath": "/static/tabs/user_selected.png"
"text": "个人中心",
"pagePath": "pages/my/userCenter",
"iconPath": "/static/tabs/my.png",
"selectedIconPath": "/static/tabs/my_active.png"
}
]
},
"subPackages": [
"subPackages": [{
"root": "pagesOther",
"pages": [{
"path": "pages/blank/rich-text",
"style": {
"navigationBarTitleText": ""
// #ifdef WEB
"navigationStyle": "custom"
// #endif
}
}]
},
// company
{
"root": "pagesGoods",
"pages": [
{
"path": "pages/goods/goods",
"root": "pagesCompany",
"pages": [{
"path": "pages/companyPortrait/index",
"style": {
"navigationBarTitleText": "企业画像",
// #ifdef WEB
"navigationStyle": "custom"
// #endif
}
}]
},
// custome
{
"root": "pagesCustomer",
"pages": [{
"path": "pages/customerManagement/index",
"style": {
"navigationBarTitleText": "商品详情",
"navigationBarTitleText": "客户管理",
"navigationStyle": "custom"
}
},
{
"path": "pages/customsExpress/index",
"style": {
"navigationBarTitleText": "",
// #ifdef WEB
"navigationStyle": "custom"
// #endif
}
},
{
"path": "pages/list/list",
"path": "pages/customsService/index",
"style": {
"navigationBarTitleText": "商品列表",
// #ifdef WEB
"navigationStyle": "custom"
// #endif
}
},
{
"path": "pages/evaluate/evaluate",
"style": {
"navigationBarTitleText": "商品评价",
// #ifdef WEB
"navigationStyle": "custom"
// #endif
}
},
{
"path": "pages/search/search",
"style": {
"navigationBarTitleText": "搜索商品",
// #ifdef WEB
"navigationStyle": "custom"
// #endif
"navigationBarTitleText": "智慧关务",
"navigationBarBackgroundColor": "#fff"
}
}
]
},
// shop
{
"root": "pagesMember",
"root": "pagesShop",
"pages": [
//
{
"path": "pages/login/login",
"style": {
@ -135,15 +163,6 @@
// #endif
}
},
{
"path": "pages/settings/settings",
"style": {
"navigationBarTitleText": "设置",
// #ifdef WEB
"navigationStyle": "custom"
// #endif
}
},
{
"path": "pages/profile/profile",
"style": {
@ -152,6 +171,15 @@
"navigationBarTitleText": "个人信息"
}
},
{
"path": "pages/settings/settings",
"style": {
"navigationBarTitleText": "设置",
// #ifdef WEB
"navigationStyle": "custom"
// #endif
}
},
{
"path": "pages/address/address",
"style": {
@ -214,162 +242,212 @@
"navigationStyle": "custom"
// #endif
}
}
]
},
{
"root": "pagesOrder",
"pages": [
},
//
{
"path": "pages/create/create",
"path": "pages/shopMall/index",
"style": {
"navigationBarTitleText": "大贸商城",
// #ifdef WEB
"navigationStyle": "default"
// #endif
}
},
{
"path": "pages/shopGoods/goods/goods",
"style": {
"navigationBarTitleText": "商品详情",
// #ifdef WEB
"navigationStyle": "default"
// #endif
}
},
{
"path": "pages/shopGoods/list/list",
"style": {
"navigationBarTitleText": "商品列表",
// #ifdef WEB
"navigationStyle": "default"
// #endif
}
},
{
"path": "pages/shopGoods/evaluate/evaluate",
"style": {
"navigationBarTitleText": "商品评价",
// #ifdef WEB
"navigationStyle": "default"
// #endif
}
},
{
"path": "pages/shopGoods/search/search",
"style": {
"navigationBarTitleText": "搜索商品",
// #ifdef WEB
"navigationStyle": "default"
// #endif
}
},
//
{
"path": "pages/shopCart/cart/cart",
"style": {
"navigationBarTitleText": "购物车",
// #ifdef WEB
"navigationStyle": "default"
// #endif
}
},
//
{
"path": "pages/shopCategory/category/category",
"style": {
"navigationBarTitleText": "分类",
// #ifdef WEB
"navigationStyle": "default"
// #endif
}
},
//
{
"path": "pages/shopCreate/create",
"style": {
"navigationBarTitleText": "订单创建",
// #ifdef WEB
"navigationStyle": "custom"
"navigationStyle": "default"
// #endif
}
},
{
"path": "pages/detail/detail",
"path": "pages/shopDetail/detail",
"style": {
"navigationBarTitleText": "订单详情",
// #ifdef WEB
"navigationStyle": "custom"
"navigationStyle": "default"
// #endif
}
},
{
"path": "pages/payment/pay",
"path": "pages/shopPayment/pay",
"style": {
"navigationBarTitleText": "订单支付",
// #ifdef WEB
"navigationStyle": "custom"
"navigationStyle": "default"
// #endif
}
},
{
"path": "pages/payment/payment",
"path": "pages/shopPayment/payment",
"style": {
"navigationBarTitleText": "支付结果",
// #ifdef WEB
"navigationStyle": "custom"
"navigationStyle": "default"
// #endif
}
},
{
"path": "pages/list/list",
"path": "pages/shopList/list",
"style": {
"navigationBarTitleText": "订单列表",
// #ifdef WEB
"navigationStyle": "custom"
"navigationStyle": "default"
// #endif
}
},
{
"path": "pages/receive/receive",
"path": "pages/shopReceive/receive",
"style": {
"navigationBarTitleText": "收货结果",
// #ifdef WEB
"navigationStyle": "custom"
"navigationStyle": "default"
// #endif
}
},
{
"path": "pages/evaluate/evaluate",
"path": "pages/shopEvaluate/evaluate",
"style": {
"navigationBarTitleText": "评价订单",
// #ifdef WEB
"navigationStyle": "custom"
"navigationStyle": "default"
// #endif
}
},
{
"path": "pages/refund/refund",
"path": "pages/shopRefund/refund",
"style": {
"navigationBarTitleText": "申请售后",
// #ifdef WEB
"navigationStyle": "custom"
"navigationStyle": "default"
// #endif
}
},
{
"path": "pages/refund/list",
"path": "pages/shopRefund/list",
"style": {
"navigationBarTitleText": "售后列表",
// #ifdef WEB
"navigationStyle": "custom"
"navigationStyle": "default"
// #endif
}
},
{
"path": "pages/refund/detail",
"path": "pages/shopRefund/detail",
"style": {
"navigationBarTitleText": "售后详情",
// #ifdef WEB
"navigationStyle": "custom"
"navigationStyle": "default"
// #endif
}
},
{
"path": "pages/delivery/delivery",
"path": "pages/shopDelivery/delivery",
"style": {
"navigationBarTitleText": "物流详情",
// #ifdef WEB
"navigationStyle": "custom"
"navigationStyle": "default"
// #endif
}
},
{
"path": "pages/recharge/list",
"path": "pages/shopRecharge/list",
"style": {
"navigationBarTitleText": "充值订单",
// #ifdef WEB
"navigationStyle": "custom"
"navigationStyle": "default"
// #endif
}
},
{
"path": "pages/center/center",
"path": "pages/shopCenter/center",
"style": {
"navigationBarTitleText": "订单中心",
// #ifdef WEB
"navigationStyle": "custom"
// #endif
}
}
]
},
{
"root": "pagesOther",
"pages": [
{
"path": "pages/blank/rich-text",
"style": {
"navigationBarTitleText": "",
// #ifdef WEB
"navigationStyle": "custom"
"navigationStyle": "default"
// #endif
}
}
]
}
],
"preloadRule": {
"pages/index/index": {
"pages/home/index": {
"network": "all",
"packages": ["pagesGoods"]
"packages": ["pagesShop"]
},
"pages/category/category": {
"pages/assistant/index": {
"network": "all",
"packages": ["pagesGoods"]
"packages": ["pagesShop"]
},
"pages/cart/cart": {
"pages/todo/index": {
"network": "all",
"packages": ["pagesGoods"]
"packages": ["pagesShop"]
},
"pages/my/my": {
"pages/my/userCenter": {
"network": "all",
"packages": ["pagesMember", "pagesOrder"]
"packages": ["pagesShop", "pagesCompany"]
}
}
}

View File

@ -0,0 +1,9 @@
<template>
<view>
<text>海保助手</text>
</view>
</template>
<script lang="ts" setup></script>
<style lang="scss" scoped></style>

297
src/pages/black/index.vue Normal file
View File

@ -0,0 +1,297 @@
<template>
<!-- 黑名单页面 -->
<view class="page-container">
<!-- 导航栏 -->
<wd-navbar title="账户状态" safeAreaInsetTop :bordered="false"></wd-navbar>
<!-- 主要内容区域 -->
<view class="content-wrapper">
<!-- 状态图标 -->
<view class="status-icon-wrapper">
<view class="icon-bg">
<wd-icon name="warning" size="100rpx" color="#fff"></wd-icon>
</view>
</view>
<!-- 标题 -->
<view class="title-text">账户已被限制</view>
<!-- 描述信息 -->
<view class="description-text">
<text>{{blackInfo.content}}</text>
</view>
<!-- 联系客服卡片 -->
<view class="contact-card-new">
<!-- <view class="contact-header">
<uv-icon name="server-man" size="36rpx" color="#389054"></uv-icon>
<text class="header-title">联系我们</text>
</view> -->
<view class="contact-body">
<view class="contact-item-new">
<image class="item-img" :src="blackInfo.qrCode" mode="widthFix"></image>
</view>
</view>
<view class="contact-footer">
<wd-icon name="time" size="28rpx" color="#999"></wd-icon>
<text class="footer-text">服务时间{{blackInfo.date}}</text>
</view>
</view>
</view>
<!-- 底部提示 -->
<view class="footer-tips">
<view class="tip-card">
<uv-icon name="info-circle" size="32rpx" color="#f9ae3d"></uv-icon>
<view class="tip-content">
<view class="tip-title">温馨提示</view>
<view class="tip-text">账户异常可能由违规操作安全风险等原因导致请您理解与配合</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import {
ref,
onMounted
} from "vue";
interface blackObj {
//ID
status : number
content : string
//
qrCode : string
//
date : string
}
const blackInfo = ref<blackObj>({});
onMounted(() => {
uni.getStorage({
key: 'blackInfo',
success: (res) => {
console.log(res.data);
blackInfo.value = res.data;
}
})
})
// //
// const phoneValue = ref("");
// //
// const makePhoneCall = () => {
// //
// if (!phoneValue.value) {
// uni.showToast({
// title: "",
// icon: "none",
// });
// return;
// }
// uni.makePhoneCall({
// phoneNumber: phoneValue.value,
// success: () => {
// console.log("");
// },
// fail: (err) => {
// console.error(":", err);
// uni.showToast({
// title: "",
// icon: "none",
// });
// },
// });
// };
// //
// const handleContactService = () => {
// //
// uni.vibrateShort && uni.vibrateShort();
// makePhoneCall();
// };
// //
// const getSetting = async () => {
// try {
// const { kefu } = await useHttp("/api/setting");
// if (kefu && kefu.kefu_mobile) {
// phoneValue.value = kefu.kefu_mobile;
// } else {
// console.warn("");
// }
// } catch (e) {
// console.error("", e);
// }
// };
// //
// getSetting();
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background: linear-gradient(180deg, #f0f8f2 0%, #ffffff 40%);
display: flex;
flex-direction: column;
}
.content-wrapper {
// flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 40rpx;
box-sizing: border-box;
.status-icon-wrapper {
margin-top: 60rpx;
margin-bottom: 40rpx;
position: relative;
.icon-bg {
width: 160rpx;
height: 160rpx;
border-radius: 50%;
background: linear-gradient(145deg, #ff7575, #dd524d);
box-shadow: 0 10rpx 30rpx rgba(221, 82, 77, 0.4);
display: flex;
justify-content: center;
align-items: center;
animation: pulse 2s infinite;
}
}
.title-text {
font-size: 48rpx;
font-weight: bold;
color: #303133;
margin-bottom: 24rpx;
}
.description-text {
font-size: 30rpx;
color: #606266;
line-height: 1.6;
text-align: center;
margin-bottom: 60rpx;
padding: 0 20rpx;
}
.contact-card-new {
width: 100%;
background-color: #fff;
border-radius: 24rpx;
padding: 32rpx;
box-sizing: border-box;
box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.08);
// margin-bottom: 60rpx;
.contact-header {
display: flex;
align-items: center;
margin-bottom: 32rpx;
.header-title {
font-size: 36rpx;
font-weight: bold;
color: #303133;
margin-left: 16rpx;
}
}
.contact-body {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.contact-item-new {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx;
background-color: #f7f8fa;
border-radius: 16rpx;
transition: all 0.2s ease;
&:active {
background-color: #f2f3f5;
}
.item-image {
width: 100%;
}
}
.contact-footer {
display: flex;
align-items: center;
justify-content: center;
padding-top: 24rpx;
margin-top: 24rpx;
border-top: 1rpx solid #f2f3f5;
.footer-text {
font-size: 26rpx;
color: #909399;
margin-left: 12rpx;
}
}
}
}
.footer-tips {
padding: 0 40rpx 40rpx;
box-sizing: border-box;
.tip-card {
display: flex;
align-items: flex-start;
background-color: #fff7e8;
border-radius: 16rpx;
padding: 24rpx;
.tip-content {
margin-left: 16rpx;
.tip-title {
font-size: 28rpx;
font-weight: bold;
color: #f29100;
margin-bottom: 8rpx;
}
.tip-text {
font-size: 26rpx;
color: #b87a28;
line-height: 1.5;
}
}
}
}
@keyframes pulse {
0% {
transform: scale(1);
box-shadow: 0 10rpx 30rpx rgba(221, 82, 77, 0.4);
}
50% {
transform: scale(1.05);
box-shadow: 0 15rpx 40rpx rgba(221, 82, 77, 0.6);
}
100% {
transform: scale(1);
box-shadow: 0 10rpx 30rpx rgba(221, 82, 77, 0.4);
}
}
</style>

View File

@ -0,0 +1,97 @@
<template>
<view class="service-grid">
<view class="grid-container">
<view
v-for="(service, index) in props.list"
:key="index"
class="service-item"
@click="handleServiceClick(service)"
>
<view class="service-icon flex-row align-center justify-center">
<image
:src="service.img"
mode="aspectFit"
:class="getIconClass(1)"
></image>
</view>
<text class="service-title">{{ service.name }}</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { pageUrl } from '@/utils/constants'
import type { menuItem } from '@/types/home'
interface ServiceItem {
title: string
iconPath: string
route?: string
floor?: number
}
//
const props = withDefaults(
defineProps<{
list: menuItem[]
}>(),
{
},
)
const handleServiceClick = (service: ServiceItem) => {
if (service.url && service.url != "#") {
uni.navigateTo({
url: service.url,
})
}
}
const getIconClass = (floor: number | undefined) => {
return floor === 1 ? 'icon-size-1' : floor === 2 ? 'icon-size-2' : 'icon-size-3'
}
</script>
<style lang="scss" scoped>
.service-grid {
.grid-container {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 40rpx;
}
.service-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 10rpx;
cursor: pointer;
transition: transform 0.2s ease;
.service-icon {
.icon-size-1 {
width: 80rpx;
height: 80rpx;
}
.icon-size-2 {
width: 50rpx;
height: 50rpx;
}
.icon-size-3 {
width: 50rpx;
height: 50rpx;
}
}
.service-title {
font-size: 24rpx;
color: #333;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
</style>

View File

@ -0,0 +1,881 @@
<template>
<view class="tab-content">
<!-- 推荐 tab - 商品展示 -->
<view v-if="activeTab === 'goods'" class="recommend-content">
<view class="goods-list flex-col">
<view
v-for="(item,index) in dataList"
:key="index"
class="goods-item"
@click="jumpUrl(item.url)"
>
<view class="goods-image">
<image :src="item.cover" mode="aspectFill"></image>
</view>
<view class="goods-info flex-col">
<text class="goods-title">{{ item.name }}</text>
<view class="goods-tags">
<text v-for="tag in item.tags" :key="tag" class="goods-tag">{{ tag }}</text>
</view>
</view>
<view class="goods-price align-end">
<text class="price-symbol"></text>
<text class="price-value">{{ item.min_price}}</text>
<text class="price-unit">/</text>
</view>
</view>
</view>
</view>
<!-- 合作伙伴 tab -->
<view v-else-if="activeTab === 1" class="partners-content">
<view class="partners-list">
<view class="partner-item" v-for="partner in partners" :key="partner.id">
<image class="partner-logo" :src="partner.logo" mode="aspectFit"></image>
<text class="partner-name">{{ partner.name }}</text>
<text class="partner-desc">{{ partner.description }}</text>
</view>
</view>
</view>
<!-- 商机 tab -->
<view v-else-if="activeTab === 'opportunity'" class="opportunities-content"
>
<view class="opportunities-list">
<view class="opportunity-item" v-for="(opp,index) in dataList" :key="index" @click="jumpUrl(opp.url)">
<view class="opp-header">
<text class="opp-title">{{ opp.name }}</text>
<view class="opp-status active" >{{ opp.service_fee }}</view>
</view>
<view class="opp-desc">{{ opp.desc }}</view>
<view class="opp-meta">
<text class="opp-location">{{ opp.cate_name }}</text>
<text class="opp-date">{{ opp.create_time }}</text>
</view>
</view>
</view>
</view>
<!-- 关于我们 tab -->
<view v-else-if="activeTab === 3" class="about-content">
<view class="about-info">
<text class="about-text">
我们是一家专注于国际贸易和物流服务的综合性企业致力于为客户提供优质高效便捷的服务
</text>
<view class="company-stats">
<view class="stat-item">
<text class="stat-number">10+</text>
<text class="stat-label">年行业经验</text>
</view>
<view class="stat-item">
<text class="stat-number">1000+</text>
<text class="stat-label">服务客户</text>
</view>
<view class="stat-item">
<text class="stat-number">50+</text>
<text class="stat-label">合作国家</text>
</view>
</view>
</view>
</view>
<!-- 找买家 tab -->
<view v-else-if="activeTab === 'buy'" class="buyers-content">
<!-- <view class="buyers-search">
<search-bar
ref="buyersSearchBarRef"
:placeholder="'请输入搜索关键字'"
:showSearchButton="false"
@search="handleSearch"
@input="handleSearchInput"
@clear="handleClearSearch"
/>
</view> -->
<view class="buyers-list">
<view class="buyer-card flex-col" @click="jumpUrl(buyer.url)" v-for="(buyer,index) in dataList" :key="buyer.id">
<!-- 标题行 -->
<view class="card-header flex-row justify-between align-center">
<text class="product-title">{{ buyer.name }}</text>
<view class="view-more">
<text class="view-more-text">查看更多</text>
<wd-icon name="arrow-right" size="14" color="#666666"></wd-icon>
</view>
</view>
<!-- 海关编码 -->
<!-- <view class="customs-code">
<text class="code-label">海关编码</text>
<text class="code-value">{{ buyer.customsCode }}</text>
</view> -->
<view class="divider"></view>
<!-- 主要数据 -->
<view class="data-section">
<view class="data-items-combined">
<view class="data-item">
<text class="data-label1">类型</text>
<text class="data-value">{{ buyer.cate_name }}</text>
</view>
<view class="data-item">
<text class="data-label1">贸易伙伴</text>
<text class="data-value">{{ buyer.partner }}</text>
</view>
<view class="data-item">
<text class="data-label1">邮箱</text>
<text class="data-value">{{ buyer.email }}</text>
</view>
<view class="data-item">
<text class="data-label2">日期</text>
<text class="data-value">{{ buyer.date }}</text>
</view>
</view>
<view class="product-info">
<view class="data-item">
<text class="data-label1">描述</text>
<view class="data-value">{{ buyer.remarks }}</view>
</view>
</view>
</view>
<!-- 进出口商信息 -->
<view class="trade-info flex-row justify-between">
<view class="exporter-info flex-row">
<view class="exporter-item">
<view class="info-label">主营业务</view>
<view class="info-value">{{ buyer.business }}</view>
</view>
<!-- <view class="exporter-item flex-col">
<image :src="buyer.logo" mode="aspectFit" class="logo"></image>
</view> -->
</view>
<!-- 竖向分隔线 -->
<view class="vertical-divider"></view>
<view class="importer-info flex-row">
<view class="importer-item">
<view class="info-label">是否有联系方式</view>
<text class="info-value">{{ buyer.is_contact == 1 ? '有' :'无' }}</text>
</view>
<!-- <view class="importer-item flex-col">
<text class="info-value">{{ buyer.importerCountry }}</text>
<view class="info-label">所在国</view>
</view> -->
<view class="importer-item flex-col">
<image :src="buyer.logo" class="logo"></image>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 找卖家 tab -->
<view v-else-if="activeTab === 'sell'" class="sellers-content">
<view class="sellers-list">
<view class="seller-item" @click="jumpUrl(seller.url)" v-for="(seller,index) in dataList" :key="index">
<view class="seller-avatar">
<image :src="seller.logo" mode="aspectFill"></image>
</view>
<view class="seller-info">
<view class="seller-name">{{ seller.name }}</view>
<text class="seller-company">{{ seller.cate_name }}</text>
<view class="seller-products">{{ seller.remarks }}</view>
</view>
<view class="seller-action">
<text class="contact-btn">联系</text>
</view>
</view>
</view>
</view>
<!-- 保税生活 -->
<view v-else-if="activeTab === 'life'" class="sellers-content">
<view class="sellers-list">
<view class="seller-item" @click="jumpUrl(seller.url)" v-for="(seller,index) in dataList" :key="index">
<view class="seller-avatar">
<image :src="seller.logo" mode="aspectFill"></image>
</view>
<view class="seller-info">
<view class="seller-name">{{ seller.name }}</view>
<view class="tags_box">
<view class="tag_item" v-for="(t,ti) in seller.tag" :key="index">
{{t}}
</view>
</view>
<!-- <view class="seller-company">{{ setTag(seller.tag)}}</view> -->
<view class="seller-products">{{ seller.remarks }}</view>
</view>
<view class="seller-action">
<text class="contact-btn">联系</text>
</view>
</view>
</view>
</view>
<!-- 默认内容 -->
<view v-if="!dataList.length" class="default-content">
<text class="default-text">暂无内容</text>
</view>
</view>
</template>
<script setup>
import { computed, ref, reactive, watch } from 'vue'
import { configSearch } from '@/api/home'
// props
const props = defineProps({
activeTab: {
type: [String,Number],
default: 0,
},
})
const dataList = ref([])
const hasNextPage = ref(false)
watch(() => props.activeTab, (newValue, oldValue) => {
queryParams.page = 1
dataList.value = []
getList()
});
//
const recommendGoods = ref([
{
id: 1,
title: '劳斯莱斯',
image: '/static/images/home/goods1.png',
tags: ['拍卖', '进口', '展会清仓', '海关处置', '零售'],
priceType: '¥',
price: '500',
unit: 'CBM',
},
{
id: 2,
title: '优质商品B',
image: '/static/images/home/goods2.png',
tags: ['拍卖', '进口', '展会清仓', '海关处置', '零售'],
priceType: '¥',
price: '500',
unit: 'CBM',
},
])
//
const partners = ref([
{
id: 1,
name: '合作伙伴A',
logo: '/static/images/home/partner1.png',
description: '长期合作伙伴,提供优质服务',
},
{
id: 2,
name: '合作伙伴B',
logo: '/static/images/home/partner2.png',
description: '战略合作伙伴,共同发展',
},
])
//
const opportunities = ref([
{
id: 1,
title: '国际贸易合作机会',
description: '寻找长期稳定的贸易合作伙伴,共同开拓国际市场',
status: 'active',
statusText: '进行中',
location: '上海',
date: '2024-01-15',
},
{
id: 2,
title: '物流服务招标',
description: '大型物流项目招标,诚邀有实力的物流企业参与',
status: 'pending',
statusText: '待审核',
location: '深圳',
date: '2024-01-20',
},
])
//
const buyers = ref([
{
id: 1,
productName: '优质商品A',
customsCode: '12345678901234567890',
quantity: '1000',
amount: '100000',
description: '这是一款高质量的商品,适合长期合作。',
metricTons: '10',
date: '2023-12-30',
exporter: 'ABC贸易公司',
exporterCountry: '中国',
importer: 'XYZ进出口公司',
importerCountry: '美国',
},
{
id: 2,
productName: '商品B',
customsCode: '98765432109876543210',
quantity: '500',
amount: '50000',
description: '这是一款价格合理的商品,需求量大。',
metricTons: '5',
date: '2024-01-10',
exporter: 'DEF进出口公司',
exporterCountry: '英国',
importer: 'GHI贸易公司',
importerCountry: '德国',
},
])
//
const buyersSearchBarRef = ref()
const queryParams = reactive({
page:1,
page_size:10,
keywords:''
})
//
const handleSearch = (keyword) => {
console.log('找买家调用api搜索:', keyword)
//
}
//
const handleSearchInput = (keyword) => {
console.log('搜索输入:', keyword)
}
const setTag = (list)=>{
if(list && list.length){
return list.join(",")
}else{
return ""
}
}
//
const handleClearSearch = () => {
console.log('清空搜索')
}
//
const sellers = ref([
{
id: 1,
name: '王先生',
company: '优质供应商A',
avatar: '/static/images/home/avatar3.png',
products: '提供各类商品,质量保证',
},
{
id: 2,
name: '赵女士',
company: '优质供应商B',
avatar: '/static/images/home/avatar4.png',
products: '专业生产,价格优惠',
},
])
//
const handleViewMore = () => {
console.log('查看更多推荐商品')
//
}
//
const handleGoodsClick = (goods) => {
// console.log(':', goods)
uni.navigateTo({
url:goods.url
})
//
}
const jumpOppo = (data)=>{
console.log('点击商品:', data)
}
const jumpBuy = (data)=>{
console.log('点击商品:', data)
}
const jumpUrl=(url)=>{
uni.navigateTo({
url:'/'+url,
fail: () => {
uni.reLaunch({
url:'/'+url,
})
}
})
}
const getList = (page = 1 )=>{
queryParams.page = page
configSearch({
...queryParams,
key:props.activeTab
}).then(res=>{
if(res.code == 1){
if(queryParams.page == 1){
dataList.value = res.data.list
}else{
dataList.value = [...dataList.value,...res.data.list]
}
hasNextPage.vaue = dataList.value.length >= res.data.count ? false : true
}
})
}
const getMoreData = ()=>{
if(!hasNextPage.value)return
queryParams.page++
getList()
}
getList()
defineExpose({
getList,
queryParams,
getMoreData
})
</script>
<style lang="scss" scoped>
@import '@/styles/common.scss';
.tab-content {
//
.recommend-content {
.goods-list {
gap: 20rpx;
.goods-item {
display: flex;
gap: 20rpx;
padding: 30rpx;
background: #f8f8f8;
border-radius: $border-radius;
overflow: hidden;
.goods-image {
width: 150rpx;
height: 150rpx;
border-radius: 12rpx;
background: #697efe;
image{
width: 150rpx;
height: 150rpx;
border-radius: 12rpx;
}
}
.goods-info {
padding: 9rpx 0;
gap: 20rpx;
.goods-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
display: block;
}
.goods-tags {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
.goods-tag {
height: 30rpx;
line-height: 30rpx;
padding: 0 10rpx;
background: #e7e7e7;
border-radius: 8rpx;
font-weight: 400;
font-size: 20rpx;
color: #999999;
}
}
}
.goods-price {
display: flex;
font-weight: bold;
font-size: 32rpx;
color: #ff8500;
}
}
}
}
//
.partners-content {
.partners-list {
.partner-item {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
gap: 20rpx;
.partner-logo {
width: 80rpx;
height: 80rpx;
border-radius: 12rpx;
}
.partner-name {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
display: block;
}
.partner-desc {
font-size: 24rpx;
color: #666;
}
}
}
}
//
.opportunities-content {
.opportunities-list {
.opportunity-item {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
.opp-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
.opp-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
}
.opp-status {
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 22rpx;
&.active {
background: #e8f5e8;
color: #52c41a;
}
&.pending {
background: #fff7e6;
color: #fa8c16;
}
}
}
.opp-desc {
font-size: 26rpx;
color: #666;
display: -webkit-box; /* 设置为WebKit内核的弹性盒子模型 */
margin-bottom: 16rpx;
-webkit-box-orient: vertical; /* 垂直排列 */
-webkit-line-clamp: 2; /* 限制显示两行 */
overflow: hidden; /* 隐藏超出范围的内容 */
text-overflow: ellipsis; /* 使用省略号 */
width: 100%;
}
.opp-meta {
display: flex;
gap: 20rpx;
// margin-top: 10px;
.opp-location,
.opp-date {
font-size: 22rpx;
color: #999;
}
}
}
}
}
//
.about-content {
.about-info {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
.about-text {
font-size: 26rpx;
color: #666;
line-height: 1.6;
margin-bottom: 30rpx;
display: block;
}
.company-stats {
display: flex;
justify-content: space-around;
.stat-item {
text-align: center;
.stat-number {
font-size: 36rpx;
font-weight: bold;
color: #0478f4;
display: block;
margin-bottom: 8rpx;
}
.stat-label {
font-size: 22rpx;
color: #999;
}
}
}
}
}
//
.buyers-content {
.buyers-search {
margin-bottom: 20rpx;
}
.buyers-list {
.buyer-card {
background: #fff;
border-radius: $border-radius;
padding: 24rpx 29rpx;
padding-bottom: 30rpx;
margin-bottom: 20rpx;
.card-header {
margin-bottom: 20rpx;
.product-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
flex: 1;
overflow: hidden;
}
.view-more {
font-size: 24rpx;
color: #666666;
}
}
.customs-code {
display: flex;
align-items: center;
gap: 10rpx;
font-size: 24rpx;
color: #666666;
margin-bottom: 30rpx;
}
.divider {
height: 1rpx;
background: #eee;
margin-bottom: 28rpx;
}
.data-section {
.data-items-combined {
display: grid;
grid-template-columns: repeat(2, 1fr);
row-gap: 30rpx;
margin-bottom: 30rpx;
}
.product-info {
margin-bottom: 38rpx;
}
.data-item {
display: flex;
align-items: center;
gap: 10rpx;
.data-label1 {
width: 100rpx;
font-size: 24rpx;
color: #666;
}
.data-label2 {
width: 60rpx;
font-size: 24rpx;
color: #666;
}
.data-value {
// width: 200px;
display: -webkit-box; /* 设置为WebKit内核的弹性盒子模型 */
-webkit-box-orient: vertical; /* 垂直排列 */
-webkit-line-clamp: 3; /* 限制显示三行 */
overflow: hidden; /* 隐藏超出范围的内容 */
text-overflow: ellipsis; /* 使用省略号 */
font-size: 24rpx;
font-weight: 540;
color: #333;
}
}
}
.trade-info {
.exporter-info,
.importer-info {
flex: 1;
justify-content: space-between;
overflow: hidden;
.exporter-item,
.importer-item {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8rpx;
.logo{
width: 30px;
height: 30px;
}
.info-value {
width: 120px;
font-size: 24rpx;
font-weight: 600;
color: #333333;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.info-label {
font-size: 22rpx;
color: #666666;
}
}
}
.exporter-info {
padding-right: 20rpx;
}
.importer-info {
padding-left: 20rpx;
}
.vertical-divider {
width: 1rpx;
height: 80rpx;
background: #e5e5e5;
margin: 0 20rpx;
align-self: center;
}
}
}
}
}
//
.sellers-content {
.sellers-list {
.seller-item {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
gap: 20rpx;
.seller-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
overflow: hidden;
image {
width: 100%;
height: 100%;
}
}
.seller-info {
flex: 1;
overflow: hidden;
.tags_box{
width: 100%;
display: flex;
align-items: center;
flex-wrap: wrap;
.tag_item{
height: 30rpx;
line-height: 30rpx;
padding: 0 10rpx;
background: #e7e7e7;
border-radius: 8rpx;
font-weight: 400;
font-size: 20rpx;
color: #999999;
// margin-top: 10px;
margin-bottom: 5px;
margin-right: 5px;
}
}
.seller-name {
width: 100%;
font-size: 28rpx;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 8rpx;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.seller-company {
font-size: 24rpx;
color: #666;
display: block;
margin-bottom: 8rpx;
}
.seller-products {
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 22rpx;
color: #999;
}
}
.seller-action {
.contact-btn {
background: #0478f4;
color: #fff;
padding: 12rpx 24rpx;
border-radius: 20rpx;
font-size: 24rpx;
}
}
}
}
}
//
.default-content {
text-align: center;
padding: 100rpx 0;
.default-text {
font-size: 28rpx;
color: #999;
}
}
}
</style>

View File

@ -0,0 +1,115 @@
<template>
<view class="tab-navigation">
<scroll-view class="tab-scroll" scroll-x="true" show-scrollbar="false">
<view class="tab-list">
<view
v-for="(tab, index) in switchList"
:key="index"
class="tab-item"
:class="{ active: activeTab === index }"
@click="handleTabClick(index, tab)"
>
<text class="tab-text">{{ tab.name }}</text>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
// TabItemswitchList
interface TabItem {
name: string
keys: string
route: string
}
// propsswitchListactiveTab
const props = defineProps({
switchList: {
type: Array as () => TabItem[],
default: () => []
},
activeTab: {
type: Number,
default: 0
}
})
const handleTabClick = (index: number, tab: TabItem) => {
//
emit('tabChange', { index, tab })
}
//
const emit = defineEmits<{
tabChange: [{ index: number; tab: TabItem }]
}>()
</script>
<style lang="scss" scoped>
.tab-navigation {
.tab-scroll {
white-space: nowrap;
overflow: hidden; //
// - Uni-app
::v-deep .uni-scroll-view::-webkit-scrollbar {
display: none;
}
::v-deep .uni-scroll-view {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
}
.tab-list {
display: flex;
gap: 50rpx;
}
.tab-item {
flex-shrink: 0;
transition: all 0.3s ease;
cursor: pointer;
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: 8rpx;
&.active {
.tab-text {
color: #333333;
font-weight: bold;
position: relative;
z-index: 2;
}
//
&::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 40rpx;
height: 40rpx;
background: linear-gradient(0deg, #0478f4 0%, rgba(4, 120, 244, 0) 100%);
border-radius: 50%;
z-index: 1;
}
}
.tab-text {
font-size: 28rpx;
color: #666;
font-weight: 500;
transition: color 0.3s ease;
}
}
}
</style>

192
src/pages/home/index.vue Normal file
View File

@ -0,0 +1,192 @@
<template>
<view class="home-page" :style="backgroundStyle">
<!-- 顶部渐变背景 -->
<view :style="topGradientStyle"></view>
<!-- logo -->
<!-- <image src="/static/images/logo.png" class="home-page-logo"></image> -->
<!-- 搜索栏 -->
<view class="search-section">
<search-bar ref="searchBarRef" :placeholder="'请输入搜索关键字'" :showSearchButton="true" @search="handleSearch"
@input="handleSearchInput" @clear="handleClearSearch" />
</view>
<!-- 轮播横幅 -->
<view class="banner-section">
<shop-swiper v-if="homeData.adv" :isFullUrl="true" :height="250" :list="homeData.adv" />
</view>
<!-- 服务网格 -->
<view class="service-grid-section">
<service-grid v-if="homeData.menu" :list="homeData.menu" />
</view>
<!-- 公告栏 -->
<view class="notice-section">
<notice-bar v-if="homeData.notice" :noticeData="homeData.notice" :prefix="true" :suffix="true" />
</view>
<!-- 导航标签 -->
<view class="tab-section">
<tab-navigation v-if="homeData.switchList" :activeTab="activeTab" :switchList="homeData.switchList"
@tabChange="handleTabChange" />
</view>
<!-- 热门推荐 -->
<view class="hot-recommend-section">
<tab-content :activeTab="activeKey" ref="tabContentRef" />
</view>
</view>
</template>
<script setup lang="ts">
import { onPullDownRefresh, onReachBottom } from "@dcloudio/uni-app"
import { onMounted, onUnmounted, ref } from 'vue'
import { useHomePage } from '@/composables/useHomePage'
import SearchBar from '@/components/search-bar/search-bar.vue'
import NoticeBar from '@/components/notice-bar.vue'
import ServiceGrid from './components/service-grid.vue'
import TabNavigation from './components/tab-navigation.vue'
import TabContent from './components/tab-content.vue'
// 使
const {
backgroundStyle,
topGradientStyle,
isRefreshing,
isLoading,
hasMore,
currentPage,
bannerList,
noticeBarList,
categoryList,
hotRecommendList,
initPage,
onRefresh,
onLoadMore,
resetState,
homeData
} = useHomePage()
//
const searchBarRef = ref()
const tabContentRef = ref(null)
// tab
const activeTab = ref(0)
const activeKey = ref('goods')
// tab
const handleTabChange = ({ index, tab } : { index : number; tab : any }) => {
activeTab.value = index
activeKey.value = tab.keys
// tab
}
//
const handleSearch = (keyword : string) => {
//
}
//
const handleSearchInput = (keyword : string) => {
tabContentRef.value.queryParams.keywords = keyword
tabContentRef.value.getList()
}
//
const handleClearSearch = () => {
tabContentRef.value.queryParams.keywords = ''
tabContentRef.value.getList()
console.log('清空搜索')
}
//
onMounted(async () => {
await initPage()
//
if (homeData.value.black.status == 1) {
uni.redirectTo({
url: '/pages/black/index'
})
return
}
//
if (homeData.value.hbsm_notice.status == 1) {
uni.showModal({
title: homeData.value.hbsm_notice.title,
content: homeData.value.hbsm_notice.content,
});
}
})
onUnmounted(() => {
resetState()
})
onPullDownRefresh(() => {
console.log("下拉刷新")
})
onReachBottom(() => {
tabContentRef.value.getMoreData()
})
</script>
<style lang="scss" scoped>
@import '@/styles/common.scss';
.home-page {
position: relative;
min-height: 100vh;
padding: 30rpx 32rpx;
// 60rpx-100rpx
padding-bottom: 60rpx;
&-logo {
width: 60rpx;
height: 60rpx;
position: relative;
z-index: 1;
margin: 20rpx 0;
}
}
.status-bar {
height: 88rpx;
background: transparent;
position: relative;
z-index: 2;
}
.capsule-menu-section {
height: 100rpx;
}
.search-section {
position: relative;
z-index: 2;
margin: 0 0 20rpx;
}
.banner-section {
position: relative;
z-index: 2;
margin-bottom: 22rpx;
}
.service-grid-section {
margin-bottom: 32rpx;
}
.notice-section {
margin-bottom: 40rpx;
}
.tab-section {
margin-bottom: 40rpx;
}
.hot-recommend-section {
margin-bottom: 44rpx;
}
</style>

View File

@ -1,109 +0,0 @@
<script setup lang="ts">
import { nextTick, ref } from 'vue'
import { getBannerApi, getHotRecommendApi, getNoticeBartApi } from '@/api/home'
import type { bannerItem, noticeBarItem } from '@/types/home'
import { onLoad, onShow } from '@dcloudio/uni-app'
import categoryGroup from '@/pages/index/components/categoryGroup.vue'
import noticeBar from '@/pages/index/components/notice-bar.vue'
import { getTopCategoryApi } from '@/api/catogory'
import { useCartStore, useMemberStore } from '@/stores'
import { getCartTotalNumApi } from '@/api/cart'
import hotRecommend from '@/pages/index/components/hotRecommend.vue'
//
const bannerList = ref<bannerItem[]>([])
const getBannerListData = async () => {
const res = await getBannerApi()
bannerList.value = res.result
}
//
const noticeBarList = ref<noticeBarItem[]>([])
const getNoticeBarData = async () => {
const res = await getNoticeBartApi()
noticeBarList.value = res.result
}
//
const categoryList = ref()
const getTopCategoryListData = async () => {
const res = await getTopCategoryApi()
categoryList.value = res.result
}
//
const hotRecommendList = ref()
const getHotRecommend = async () => {
const res = await getHotRecommendApi()
hotRecommendList.value = res.result
}
//
const shopGoodsListRef = ref()
const getIndexData = () => {
return Promise.all([
getBannerListData(),
getNoticeBarData(),
getTopCategoryListData(),
getHotRecommend(),
])
}
onLoad(() => {
getIndexData()
//
// #ifdef MP-WEIXIN
uni.showShareMenu({
withShareTicket: true,
menus: ['shareAppMessage'],
})
// #endif
})
onShow(() => {
nextTick(() => {
shopGoodsListRef.value.refresh()
})
const cartStore = useCartStore()
//
if (useMemberStore().profile) {
getCartTotalNumApi().then((res) => {
cartStore.setCartTotalNum(res.result)
})
} else {
cartStore.setCartTotalNum(0)
}
cartStore.setCartTabBadge()
})
const handleRefresh = async (status: number) => {
//
if (status === 2) {
getIndexData()
}
}
</script>
<template>
<shop-goods-list
ref="shopGoodsListRef"
@refresher-status-changed="handleRefresh"
:options="{
auto: false,
}"
>
<template #top>
<shop-goods-search />
</template>
<template #middle>
<shop-swiper :list="bannerList" v-if="bannerList" />
<noticeBar :list="noticeBarList" />
<category-group :list="categoryList" v-if="categoryList" />
<hot-recommend :list="hotRecommendList" />
<uni-section title="商品列表" type="line"></uni-section>
</template>
</shop-goods-list>
</template>
<style lang="scss">
page {
background-color: #f9f9f9;
}
</style>

View File

@ -1,61 +1,37 @@
<script setup lang="ts">
const props = withDefaults(
defineProps<{
data: {
name: string
url: string
icon: string
iconPath: string
count: string
}[]
perRow?: number
bigSpace?: boolean
}>(),
{
data: () => [],
perRow: 4,
bigSpace: false,
},
)
const props = withDefaults(
defineProps<{
data : {
name : string
url : string
icon : string
iconPath : string
count : string
}[]
perRow ?: number
bigSpace ?: boolean
}>(),
{
data: () => [],
perRow: 4,
bigSpace: false,
},
)
</script>
<template>
<view class="block">
<slot name="header"></slot>
<view class="section">
<view
class="section-item"
v-for="(item, index) in props.data"
:key="index"
hover-class="none"
:class="[
<view class="section-item" v-for="(item, index) in props.data" :key="index" hover-class="none" :class="[
{ 'no-padding-bottom': index === props.data.length - 1 },
props.perRow > 4 ? 'quintet' : '',
props.bigSpace ? 'big-space' : '',
]"
>
]">
<uni-badge :text="item.count" absolute="rightTop">
<navigator
v-if="item.iconPath"
:url="item.url"
hover-class="none"
class="navigator icon-container"
:class="props.perRow > 4 ? 'quintet' : ''"
>
<!-- #ifdef WEB -->
<img :src="`.${item.iconPath}`" alt="icon" style="width: 60rpx; height: 60rpx" />
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<img :src="`${item.iconPath}`" alt="icon" style="width: 60rpx; height: 60rpx" />
<!-- #endif -->
<navigator v-if="item.img" :url="item.url" hover-class="none" class="navigator icon-container"
:class="props.perRow > 4 ? 'quintet' : ''">
<image :src="item.img" alt="icon" style="width: 50rpx; height: 50rpx" />
</navigator>
<navigator
v-else
:url="item.url"
hover-class="none"
class="navigator icon-container"
:class="[item.icon, props.perRow > 4 ? 'quintet' : '']"
/>
</uni-badge>
<view class="section-item-text">{{ item.name }}</view>
</view>
@ -64,78 +40,86 @@ const props = withDefaults(
</view>
</template>
<style lang="scss">
.block {
position: relative;
z-index: 99;
padding: 10rpx;
margin: 20rpx;
background-color: #fff;
border-radius: 20rpx;
box-shadow: 0 4rpx 6rpx rgba(240, 240, 240, 0.6);
.block {
position: relative;
z-index: 99;
.section {
width: 100%;
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
padding-top: 30rpx;
padding-bottom: 20rpx;
.navigator {
.section {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
font-size: 24rpx;
color: #333;
&::before {
display: block;
font-size: 60rpx;
color: #ff9545;
}
flex-wrap: wrap;
justify-content: flex-start;
padding: 0 30rpx;
.icon-container {
.navigator {
display: flex;
justify-content: center;
align-items: center;
width: 60rpx;
height: 60rpx;
font-size: 24rpx;
color: #333;
&::before {
display: block;
font-size: 60rpx;
color: #ff9545;
}
.icon-container {
display: flex;
justify-content: center;
align-items: center;
width: 60rpx;
height: 60rpx;
}
.icon {
display: flex;
align-items: center;
align-content: center;
justify-content: center;
font-size: 60rpx;
width: 60rpx;
height: 60rpx;
color: #ff9545;
}
}
.icon {
&-item {
width: 100%;
flex-basis: 25%;
display: flex;
flex-direction: column;
align-items: center;
align-content: center;
justify-content: center;
font-size: 60rpx;
width: 60rpx;
height: 60rpx;
color: #ff9545;
justify-content: space-between;
margin-bottom: 20rpx;
.section-item-text {
width: 100rpx;
text-align: center;
padding-top: 10rpx;
font-size: 24rpx;
color: #333;
white-space: nowrap;
/* 强制文字不换行 */
overflow: hidden;
/* 超出容器部分隐藏 */
text-overflow: ellipsis;
/* 溢出部分显示省略号 */
}
}
}
&-item {
flex-basis: 25%;
display: flex;
flex-direction: column;
align-items: center;
.section-item-text {
padding-top: 10rpx;
font-size: 28rpx;
.quintet {
flex-basis: 20%;
}
}
.quintet {
flex-basis: 20%;
}
.big-space {
padding-bottom: 40rpx;
.big-space {
padding-bottom: 40rpx;
&:nth-last-child(-n + 4) {
padding-bottom: 20rpx;
&:nth-last-child(-n + 4) {
padding-bottom: 20rpx;
}
}
}
}
}
</style>

View File

@ -1,442 +0,0 @@
<script setup lang="ts">
import { pageUrl } from '@/utils/constants'
import { getDashboardApi } from '@/api/member'
import { useAddressStore, useMemberStore, useSettingStore } from '@/stores'
import { computed, ref } from 'vue'
import type ZPagingInstance from '@/uni_modules/z-paging/global'
import { onLoad, onPullDownRefresh, onShow } from '@dcloudio/uni-app'
import { onShowRefreshData } from '@/utils/common'
import { reactive } from 'vue'
import blockIcon from '@/pages/my/components/blockIcon.vue'
const memberStore = useMemberStore()
const addressStore = useAddressStore()
const settingStore = useSettingStore()
addressStore.changeScene('list')
const dashboard = reactive({
rights: {
balance: '0',
coupon_num: '0',
favorite_num: '0',
},
order_bar: {
to_pay: '',
to_ship: '',
to_receive: '',
to_evaluate: '',
refund: '',
},
coupon: {
eligibility: true,
},
})
const orderSection = computed(() => {
return [
{
name: '待付款',
icon: 'icon-to-pay',
count: dashboard.order_bar.to_pay,
url: `${pageUrl['order-list']}?status=1`,
},
{
name: '待发货',
icon: 'icon-to-ship',
count: dashboard.order_bar.to_ship,
url: `${pageUrl['order-list']}?status=2`,
},
{
name: '待收货',
icon: 'icon-to-receive',
count: dashboard.order_bar.to_receive,
url: `${pageUrl['order-list']}?status=3`,
},
{
name: '待评价',
icon: 'icon-to-evaluate',
count: dashboard.order_bar.to_evaluate,
url: `${pageUrl['order-list']}?status=4`,
},
{
name: '退款/售后',
icon: 'icon-refund',
count: dashboard.order_bar.refund,
url: `${pageUrl['order-refund-list']}?status=1`,
},
]
})
const couponSection = computed(() => {
return [
{
name: '待使用',
icon: 'icon-coupon-to-use',
count: '',
url: `${pageUrl['coupon']}?status=1`,
},
{
name: '已使用',
icon: 'icon-coupon-used',
count: '',
url: `${pageUrl['coupon']}?status=2`,
},
{
name: '已过期',
icon: 'icon-coupon-expired',
count: '',
url: `${pageUrl['coupon']}?status=3`,
},
{
name: '领券中心',
icon: 'icon-coupon-center',
count: dashboard.coupon.eligibility ? 'New' : '',
url: `${pageUrl['coupon-center']}`,
},
]
})
const agreements = settingStore.data.agreement
const serviceSection = computed(() => {
return [
{
name: '我的地址',
iconPath: '/static/icons/my-address.svg',
count: '',
url: `${pageUrl['address']}`,
},
{
name: '充值中心',
iconPath: '/static/icons/balance.svg',
count: '',
url: `${pageUrl['balance-recharge']}`,
},
{
name: '订单中心',
iconPath: '/static/icons/order-center.svg',
count: '',
url: `${pageUrl['order-center']}`,
},
{
name: '设置',
iconPath: '/static/icons/setting.svg',
count: '',
url: `${pageUrl['setting']}`,
},
{
name: '我的收藏',
icon: 'icon-my_favorite',
iconPath: '/static/icons/my-favorite.svg',
count: '',
url: `${pageUrl['favorite']}`,
},
{
name: '用户协议',
iconPath: '/static/icons/user-agreement.svg',
count: '',
url: `${pageUrl['rich-text']}?title=用户协议&content=${encodeURIComponent(
agreements.user_agreement,
)}`,
},
{
name: '隐私协议',
iconPath: '/static/icons/privacy-agreement.svg',
count: '',
url: `${pageUrl['rich-text']}?title=隐私协议&content=${encodeURIComponent(
agreements.privacy_agreement,
)}`,
},
{
name: '关于我们',
iconPath: '/static/icons/about-us.svg',
count: '',
url: `${pageUrl['rich-text']}?title=关于我们&content=${encodeURIComponent(
agreements.about_us_agreement,
)}`,
},
]
})
const getData = () => {
return new Promise(() => {
if (memberStore.profile?.token) {
getDashboardApi().then((res) => {
Object.assign(dashboard, res.result)
})
}
})
}
onShowRefreshData(() => {
getData()
}, true)
const paging = ref<ZPagingInstance>()
const onRefresh = () => {
getData()
setTimeout(() => {
paging.value?.complete()
}, 1000)
}
</script>
<template>
<view class="viewport">
<z-paging ref="paging" refresher-only @onRefresh="onRefresh" safe-area-inset-bottom>
<template #top>
<!-- 已登录 -->
<template v-if="memberStore.profile">
<view class="profile">
<view class="overview">
<navigator :url="`${pageUrl['profile']}`" hover-class="none">
<shop-user-avatar :width="120" :url="memberStore.profile.avatar" />
</navigator>
<view class="meta">
<view class="nickname">
{{ memberStore.profile.nickname || memberStore.profile.username }}
</view>
<navigator class="extra" :url="`${pageUrl['profile']}`" hover-class="none">
<text class="update">更新个人信息</text>
</navigator>
</view>
</view>
</view>
</template>
<!-- 未登录 -->
<template v-else>
<view class="profile">
<view class="overview">
<navigator :url="`${pageUrl['login']}`" hover-class="none">
<shop-user-avatar :width="120" url="" />
</navigator>
<view class="meta">
<navigator :url="`${pageUrl['login']}`" hover-class="none" class="nickname">
未登录
</navigator>
<view class="extra">
<text class="tips">点击登录账号</text>
</view>
</view>
</view>
</view>
</template>
<!-- 权益栏 -->
<view class="rights">
<navigator
:url="`${pageUrl['balance']}`"
open-type="navigate"
hover-class="navigator-hover"
class="rights-item"
>
<text class="rights-item-num">{{ dashboard.rights.balance }}</text>
<text>账户余额</text>
</navigator>
<navigator
:url="`${pageUrl['favorite']}`"
open-type="navigate"
hover-class="navigator-hover"
class="rights-item"
>
<text class="rights-item-num">{{ dashboard.rights.favorite_num }}</text>
<text>收藏</text>
</navigator>
<navigator
:url="`${pageUrl['coupon']}`"
open-type="navigate"
hover-class="navigator-hover"
class="rights-item"
>
<text class="rights-item-num">{{ dashboard.rights.coupon_num }}</text>
<text>优惠券</text>
</navigator>
</view>
</template>
<!-- 订单栏 -->
<blockIcon :data="orderSection" :perRow="5">
<template #header>
<view class="title">
我的订单
<navigator class="navigator" :url="`${pageUrl['order-list']}`" hover-class="none">
<text>全部</text><text class="icon-right"></text>
</navigator>
</view>
</template>
</blockIcon>
<!-- 优惠券栏 -->
<blockIcon :data="couponSection" />
<!-- 服务栏 -->
<blockIcon :data="serviceSection" big-space>
<template #footer>
<!-- #ifdef MP-WEIXIN -->
<view class="section-item" hover-class="none">
<button hover-class="none" class="contact-btn" open-type="contact">
<image src="@/static/icons/contact.svg" style="width: 60rpx; height: 60rpx" />
</button>
<view class="section-item-text">{{ '联系客服' }}</view>
</view>
<!-- #endif -->
</template>
</blockIcon>
</z-paging>
</view>
</template>
<style lang="scss">
page {
height: 100%;
overflow: hidden;
background-color: #f7f7f8;
}
.viewport {
height: 100%;
background-repeat: no-repeat;
background-image: url('~@/static/images/bg.png');
background-size: 100% auto;
}
/* 用户信息 */
.profile {
margin-top: 50rpx;
position: relative;
/* #ifdef MP-WEIXIN */
padding-top: 25px;
/* #endif */
.overview {
display: flex;
height: 120rpx;
padding: 0 36rpx;
color: #fff;
}
.avatar {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
background-color: #eee;
}
.gray {
filter: grayscale(100%);
}
.meta {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
line-height: 30rpx;
padding: 16rpx 0;
margin-left: 20rpx;
}
.nickname {
max-width: 350rpx;
padding-bottom: 20rpx;
font-size: 30rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.extra {
display: flex;
font-size: 20rpx;
}
.tips {
font-size: 22rpx;
}
.update {
padding: 3rpx 10rpx 1rpx;
color: rgba(255, 255, 255, 0.8);
border: 1rpx solid rgba(255, 255, 255, 0.8);
margin-right: 10rpx;
border-radius: 30rpx;
}
.settings {
position: absolute;
bottom: -20rpx;
right: 40rpx;
font-size: 45rpx;
color: #fff;
}
}
.rights {
display: flex;
justify-content: space-around;
padding: 35rpx 40rpx;
margin: 45rpx 20rpx 0;
background-color: #fff;
border-radius: 20rpx;
box-shadow: 0 4rpx 6rpx rgba(240, 240, 240, 0.6);
/* #ifdef WEB */
padding-top: 25px;
/* #endif */
&-item {
display: flex;
flex-direction: column;
align-items: center;
font-size: 24rpx;
color: #75787d;
padding-bottom: 10rpx;
&-num {
font-size: 40rpx;
color: #ff9545;
padding-bottom: 25rpx;
}
}
}
.title {
height: 40rpx;
line-height: 40rpx;
font-size: 28rpx;
color: #1e1e1e;
padding: 10rpx 15rpx 50rpx;
font-weight: bold;
.navigator {
font-size: 24rpx;
color: #939393;
float: right;
}
}
.section-item {
flex-basis: 25%;
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 20rpx;
}
.section-item-text {
padding-top: 10rpx;
font-size: 28rpx;
}
/* #ifdef MP-WEIXIN */
.contact-btn {
display: flex;
width: 60rpx;
height: 68rpx;
padding: 0;
background-color: transparent;
&::after {
border: none;
}
}
/* #endif */
</style>

463
src/pages/my/userCenter.vue Normal file
View File

@ -0,0 +1,463 @@
<template>
<view class="viewport">
<z-paging ref="paging" refresher-only @onRefresh="onRefresh" safe-area-inset-bottom>
<template #top>
<view class="header">
<view class="title">个人中心</view>
</view>
<!-- 已登录 -->
<template v-if="memberStore.profile">
<view class="profile">
<view class="overview">
<navigator :url="`${pageUrl['profile']}`" hover-class="none">
<shop-user-avatar :width="120" :url="otersImgs.default_avatar" />
</navigator>
<view class="meta">
<view class="nickname">
{{ memberStore.profile.nickname || memberStore.profile.username }}
</view>
<navigator class="extra" :url="`${pageUrl['profile']}`" hover-class="none">
<text class="update">暂无公司</text>
<image class="edit-icon" src="/static/images/userCenter/edit.png" />
</navigator>
</view>
</view>
<image class="qr-icon" :src="otersImgs.saoyisao" />
</view>
</template>
<!-- 未登录 -->
<template v-else>
<view class="profile">
<view class="overview">
<navigator :url="`${pageUrl['login']}`" hover-class="none">
<shop-user-avatar :width="120" url="otersImgs.default_avatar" />
</navigator>
<view class="meta">
<navigator :url="`${pageUrl['login']}`" hover-class="none" class="nickname">
未登录
</navigator>
<view class="extra">
<text class="tips">点击登录账号</text>
</view>
</view>
</view>
</view>
</template>
<!-- 权益栏 -->
<view class="rights">
<image :src="otersImgs.card_background" class="rights-img"></image>
<view class="rights-nav">
<text class="rights-nav-text">{{userInfo?.project?.project_name||'暂无'}}</text>
<wd-progress :percentage="userInfo?.project?.project_progress||20" color="#A3ACFE" />
<image :src="otersImgs.qiehuan" class="rights-nav-img" />
</view>
<view class="rights-info">
<view class="rights-info-item" v-for="(item,index) in userInfo?.project.project_data" :key="index">
<navigator :url="item.url" hover-class="none">
<view class="rights-info-item-num"> {{item.value}} </view>
<view class="rights-info-item-text"> {{item.name}} </view>
</navigator>
</view>
</view>
</view>
<!-- 海保助手&数据中心 -->
<view class="data-center">
<navigator hover-class="none" class="data-center-item">
<text class="data-center-item-text">海保助手</text>
<image :src="otersImgs.haibao_background" class="data-center-img" />
</navigator>
<navigator hover-class="none" class="data-center-item">
<text class="data-center-item-text data-center-item-data">数据中心</text>
<image :src="otersImgs.data_background" class="data-center-img" />
</navigator>
</view>
</template>
<!-- 菜单栏 -->
<view class="menus">
<view v-for='(item,index) in menusList' :key="index" class="menus-item">
<view class="menus-item-title">{{item.title}}</view>
<blockIcon :data="item.menus" :perRow="5"></blockIcon>
</view>
</view>
</z-paging>
</view>
</template>
<script setup lang="ts">
import { pageUrl } from '@/utils/constants'
import { getDashboardApi } from '@/api/member'
import { useAddressStore, useMemberStore } from '@/stores'
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { onShowRefreshData } from '@/utils/common'
import { reactive } from 'vue'
import blockIcon from '@/pages/my/components/blockIcon.vue'
import { postCenterConfigApi, postUserInfoApi } from '@/api/userCenter'
const memberStore = useMemberStore()
const pararms = {
page: 1,
page_size: 10,
}
const dashboard = reactive({
rights: {
balance: '0',
coupon_num: '0',
favorite_num: '0',
},
order_bar: {
to_pay: '',
to_ship: '',
to_receive: '',
to_evaluate: '',
refund: '',
},
coupon: {
eligibility: true,
},
})
//
const menusList = ref([])
//
const otersImgs = ref({})
//
const userInfo = ref()
onLoad(() => {
getShopCenterConfig()
})
//
const getShopCenterConfig = () => {
postCenterConfigApi(pararms).then((res) => {
if (res.code == 200) {
menusList.value = res.result.menus
otersImgs.value = res.result.other
}
})
}
//
const getUserInfo = () => {
postUserInfoApi(pararms).then((res) => {
if (res.code == 200) {
userInfo.value = res.result.userInfo
}
})
}
const getData = () => {
return new Promise(() => {
if (memberStore.profile?.token) {
getDashboardApi().then((res) => {
Object.assign(dashboard, res.result)
})
}
})
}
onShowRefreshData(() => {
getData()
getUserInfo()
}, true)
const paging = ref<ZPagingInstance>()
const onRefresh = () => {
getData()
setTimeout(() => {
paging.value?.complete()
}, 1000)
}
</script>
<style lang="scss">
page {
height: 100%;
overflow: hidden;
background-color: #f7f7f8;
}
.header {
display: flex;
align-items: center;
padding: 30rpx;
margin-top: 20rpx;
.title {
width: 100%;
text-align: center;
}
}
.viewport {
height: 100%;
color: #333333;
}
/* 用户信息 */
.profile {
position: relative;
/* #ifdef MP-WEIXIN */
padding-top: 25px;
/* #endif */
.overview {
display: flex;
height: 120rpx;
padding: 0 36rpx;
}
.avatar {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
background-color: #eee;
}
.gray {
filter: grayscale(100%);
}
.meta {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
line-height: 30rpx;
padding: 16rpx 0;
margin-left: 20rpx;
}
.nickname {
max-width: 350rpx;
padding-bottom: 20rpx;
font-size: 30rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.extra {
display: flex;
font-size: 20rpx;
align-items: center;
}
.tips {
font-size: 22rpx;
}
.update {
padding: 3rpx 10rpx 1rpx;
border: 1rpx solid rgba(255, 255, 255, 0.8);
margin-right: 10rpx;
border-radius: 30rpx;
}
.edit-icon {
width: 20rpx;
height: 20rpx;
}
.settings {
position: absolute;
bottom: -20rpx;
right: 40rpx;
font-size: 45rpx;
color: #fff;
}
.qr-icon {
position: absolute;
right: 40rpx;
top: 5%;
width: 24rpx;
height: 24rpx;
}
}
.rights {
display: flex;
flex-direction: column;
justify-content: space-around;
padding: 20rpx 20rpx 120rpx;
margin: 25rpx 20rpx 0;
border-radius: 20rpx;
box-shadow: 0 4rpx 6rpx rgba(240, 240, 240, 0.6);
position: relative;
&-img {
position: absolute;
z-index: -1;
left: 0;
top: 0;
width: 100%;
height: 280rpx;
}
&-nav {
width: 100%;
color: #fff;
font-size: 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 50rpx;
&-img {
width: 22rpx;
height: 16rpx;
}
&-text {
width: 200rpx;
white-space: nowrap;
/* 强制文字不换行 */
overflow: hidden;
/* 超出容器部分隐藏 */
text-overflow: ellipsis;
/* 溢出部分显示省略号 */
}
.wd-progress {
width: 70%;
.wd-progress__bar {
height: 100%;
border-radius: 10rpx;
}
.wd-progress__outer {
background-color: #6572F8;
height: 10rpx;
}
::v-deep .wd-progress__label {
color: #fff !important;
font-size: 24rpx;
}
}
}
&-info {
width: 100%;
display: flex;
align-items: center;
justify-content: space-around;
padding: 0 50rpx;
margin-top: 20rpx;
color: #fff;
&-item {
width: 30%;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
&-num {
font-size: 28rpx;
}
&-text {
font-size: 24rpx;
}
}
}
}
.title {
height: 40rpx;
line-height: 40rpx;
font-size: 28rpx;
color: #1e1e1e;
padding: 10rpx 15rpx 50rpx;
font-weight: bold;
.navigator {
font-size: 24rpx;
color: #939393;
float: right;
}
}
.section-item {
flex-basis: 25%;
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 20rpx;
}
.section-item-text {
padding-top: 10rpx;
font-size: 28rpx;
}
/* #ifdef MP-WEIXIN */
.contact-btn {
display: flex;
width: 60rpx;
height: 68rpx;
padding: 0;
background-color: transparent;
&::after {
border: none;
}
}
/* #endif */
.data-center {
padding: 0 30rpx;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 10rpx;
&-item {
position: relative;
&-text {
position: absolute;
top: 30rpx;
left: 30rpx;
font-size: 28rpx;
color: #9A8DEB;
}
&-data {
color: #62C082;
}
}
&-img {
width: 300rpx;
height: 110rpx;
}
}
.menus {
width: 100%;
margin-top: 30rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 0 30rpx;
&-item {
width: 100%;
margin-bottom: 20rpx;
background-color: #fff;
border-radius: 20rpx;
box-shadow: 0 4rpx 6rpx rgba(240, 240, 240, 0.6);
&-title {
color: #333333;
font-size: 32rpx;
padding: 20rpx 30rpx;
}
}
}
</style>

View File

@ -0,0 +1,46 @@
<template>
<view class="px-24 py-20 box-border">
<rk-bg :status-bar="false"></rk-bg>
<view class="w-full flex justify-center">
<uv-image src="/static/logo.png" width="200rpx" height="200rpx"></uv-image>
</view>
<view class="w-full mt-20 py-20 bg-#fff shadow rd-16 flex flex-col items-center">
<view class="font-500 fs-48 color-#001A0B">为出海加速</view>
<view class="mt-20 fs-36 color-#050E19">添加海保客服</view>
<view class="mt-20">
<uv-qrcode ref="qrcode" size="300rpx" value="https://haibao.shop" :options="{'foregroundColor': '#45908F'}"></uv-qrcode>
</view>
<view class="mt-20 color-#71757A">海保世贸-一站式服务贸易平台</view>
<view class="w-324 mt-30">
<uv-button type="primary">长按二维码添加</uv-button>
</view>
</view>
<view class="mt-40 font-600 text-center">添加后您还可以获得以下福利</view>
<view class="w-full mt-40 px-20 py-24 box-border bg-#fff rd-16 shadow flex flex-wrap justify-between items-center">
<view v-for="(item, index) in funcList" :key="`func` + index" class="w-180 mb-28 flex flex-col items-center">
<!-- <uv-image :src="platformImg(item.img)" width="84rpx" height="84rpx"></uv-image> -->
<image src="/static/images/service/service01.png"></image>
<view class="mt-12 font-500 fs-24 color-#333">{{item.name}}</view>
<view class="mt-10 fs-18 color-#999">{{item.desc}}</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue';
//
let funcList = ref([
{name: '8元现金到账', desc: '注册即送入账户', img: '/stat/service01.png'},
{name: '222元优惠券', desc: '注册即送入账户', img: '/img/service02.png'},
{name: '铂金会员7天', desc: '高峰买票', img: '/img/service03.png'},
{name: '8元现金到账', desc: '一手福利消息', img: '/img/service04.png'},
{name: 'VIP抢票券', desc: '高峰买票', img: '/img/service05.png'},
{name: '一对一服务', desc: '有问题人工咨询', img: '/img/service06.png'},
])
</script>
<style lang="scss">
</style>

492
src/pages/todo/index.vue Normal file
View File

@ -0,0 +1,492 @@
<template>
<view class="todo-page" :style="backgroundStyle">
<!-- 顶部渐变背景 -->
<view :style="topGradientStyle"></view>
<view class="todo-page-content">
<wd-navbar title="待办事项" :bordered="false" safeAreaInsetTop
custom-style="background-color: transparent !important;"></wd-navbar>
<view class="top-content mt-20 cardbox flex-row align-center justify-evenly">
<view class="top-content-item">
<view class="num">99+</view>
<view class="txt1">今日待处理</view>
</view>
<view class="top-content-item">
<view class="num">3</view>
<view class="txt2">未处理</view>
</view>
<view class="top-content-item">
<view class="num">5</view>
<view class="txt3">异常</view>
</view>
<view class="top-content-item">
<view class="num">1</view>
<view class="txt4">丢件</view>
</view>
</view>
<!-- 切换 -->
<view class="tab-section">
<tab-navigation @tabChange="handleTabChange" :switchList="switchList" :activeTab="activeTab" start-color="#FFA800" end-color="rgba(4,120,244,0)"/>
</view>
<!-- 搜索区域 -->
<view class="search-input flex-row">
<view class="flex-row input-content align-center">
<wd-picker class="picker" :columns="columns" v-model="state" @confirm="handleConfirm" >
<view class="picker-cotent flex-row align-center">{{state}}
<wd-icon name="caret-down-small" size="22px"></wd-icon></view>
</wd-picker>
<input v-model="searchValue" placeholder="请输入" />
</view>
<wd-button class="search-btn">查询</wd-button>
</view>
<!-- 列表 -->
<view class="carditem cardbox">
<view class="carditem-top flex-row align-center">
<view class="time">2025-08-17 17:06:12</view>
<view class="status1">今日待处理</view>
</view>
<view class="carditem-info flex-row align-center">
<view class="title">
二手劳斯莱斯
</view>
<view class="label">海保服务</view>
</view>
<view class="carditem-bottom flex-row justify-between align-center">
<view class="carditem-bottom-item">
<view class="label">Joicom Corp</view>
<view class="val">出口商</view>
</view>
<view class="carditem-bottom-item">
<view class="label">USA</view>
<view class="val">所在国</view>
</view>
<view class="carditem-bottom-item">
<view class="label">Joicom Corp</view>
<view class="val">进口商</view>
</view>
</view>
</view>
<view class="carditem cardbox">
<view class="carditem-top flex-row align-center">
<view class="time">2025-08-17 17:06:12</view>
<view class="status2">待提货</view>
</view>
<view class="carditem-info flex-row align-center">
<view class="title">
二手劳斯莱斯
</view>
<view class="label">海保服务</view>
</view>
<view class="carditem-bottom flex-row justify-between align-center">
<view class="carditem-bottom-item">
<view class="label">Joicom Corp</view>
<view class="val">出口商</view>
</view>
<view class="carditem-bottom-item">
<view class="label">USA</view>
<view class="val">所在国</view>
</view>
<view class="carditem-bottom-item">
<view class="label">Joicom Corp</view>
<view class="val">进口商</view>
</view>
</view>
</view>
<view class="carditem cardbox">
<view class="carditem-top flex-row align-center">
<view class="time">2025-08-17 17:06:12</view>
<view class="status3">待清关</view>
</view>
<view class="carditem-info flex-row align-center">
<view class="title">
二手劳斯莱斯
</view>
<view class="label">海保服务</view>
</view>
<view class="carditem-bottom flex-row justify-between align-center">
<view class="carditem-bottom-item">
<view class="label">Joicom Corp</view>
<view class="val">出口商</view>
</view>
<view class="carditem-bottom-item">
<view class="label">USA</view>
<view class="val">所在国</view>
</view>
<view class="carditem-bottom-item">
<view class="label">Joicom Corp</view>
<view class="val">进口商</view>
</view>
</view>
</view>
<view class="carditem cardbox">
<view class="carditem-top flex-row align-center">
<view class="time">2025-08-17 17:06:12</view>
<view class="status5">历史待处理</view>
</view>
<view class="carditem-info flex-row align-center">
<view class="title">
二手劳斯莱斯
</view>
<view class="label">海保服务</view>
</view>
<view class="carditem-bottom flex-row justify-between align-center">
<view class="carditem-bottom-item">
<view class="label">Joicom Corp</view>
<view class="val">出口商</view>
</view>
<view class="carditem-bottom-item">
<view class="label">USA</view>
<view class="val">所在国</view>
</view>
<view class="carditem-bottom-item">
<view class="label">Joicom Corp</view>
<view class="val">进口商</view>
</view>
</view>
</view>
<view class="carditem cardbox">
<view class="carditem-top flex-row align-center">
<view class="time">2025-08-17 17:06:12</view>
<view class="status6">丢件</view>
<view class="status7">异常</view>
<view class="status4">待处理</view>
</view>
<view class="carditem-info flex-row align-center">
<view class="title">
二手劳斯莱斯
</view>
<view class="label">海保服务</view>
</view>
<view class="carditem-bottom flex-row justify-between align-center">
<view class="carditem-bottom-item">
<view class="label">Joicom Corp</view>
<view class="val">出口商</view>
</view>
<view class="carditem-bottom-item">
<view class="label">USA</view>
<view class="val">所在国</view>
</view>
<view class="carditem-bottom-item">
<view class="label">Joicom Corp</view>
<view class="val">进口商</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { useTodoPage } from '@/composables/useTodoPage'
import TabNavigation from '../home/components/tab-navigation.vue'
const switchList = ref([{
name: '全部',
keys: '1',
route: '1'
}, {
name: '海报服务',
keys: '1',
route: '1'
}, {
name: '商机',
keys: '1',
route: '1'
}, {
name: '关务',
keys: '1',
route: '1'
}, {
name: '商城',
keys: '1',
route: '1'
}, {
name: '竞拍',
keys: '1',
route: '1'
}, {
name: '海运',
keys: '1',
route: '1'
}]);
const columns = ref(['状态', '选项2', '选项3', '选项4', '选项5', '选项6', '选项7'])
const state = ref('状态')
const searchValue = ref('');
const handleConfirm = ({ value: string }) => {
console.log(value)
state.value = value
}
const {
backgroundStyle,
topGradientStyle,
isRefreshing,
isLoading,
hasMore,
currentPage,
bannerList,
noticeBarList,
categoryList,
hotRecommendList,
menuList,
// switchList,
systemInfo,
noticeInfo,
hbsmNotice,
blackInfo,
activeTab,
activeTabKey,
tabContentList,
tabLoading,
tabCurrentPage,
tabHasMore,
initPage,
onRefresh,
onLoadMore,
resetState,
switchTab,
loadTabContent
} = useTodoPage()
// tab
const handleTabChange = ({ index, tab } : { index : number; tab : any }) => {
console.log('Tab changed:', index, tab.name)
// tabkey
const tabKeyMap : { [key : string] : string } = {
'推荐': 'goods',
'商机': 'opportunity',
'找买家': 'buy',
'找卖家': 'sell',
'保税生活': 'life'
}
const tabKey = tabKeyMap[tab.name] || 'goods'
switchTab(index, tabKey)
}
</script>
<style lang="scss" scoped>
.todo-page {
position: relative;
min-height: 100vh;
// 60rpx-100rpx
padding-bottom: 60rpx;
.todo-page-content{
padding: 0 32rpx;
position: sticky;
top: 0;
z-index: 100;
width: 100vw;
box-sizing: border-box;
}
}
.cardbox {
background: #FFFFFF;
box-shadow: 0rpx 6rpx 34rpx 1rpx rgba(194, 148, 56, 0.1);
border-radius: 24rpx;
}
.top-content {
padding: 50rpx 0;
.top-content-item {
text-align: center;
position: relative;
padding: 0 39rpx;
// flex:1;
&:not(:last-child) {
&::after {
display: block;
position: absolute;
right: 0;
top: 10%;
content: '';
width: 1px;
height: 80%;
background: #EFF0F2;
}
}
.num {
font-family: Alibaba PuHuiTi;
font-weight: bold;
font-size: 40rpx;
color: #333333;
}
[class^='txt'] {
font-family: Alibaba PuHuiTi;
font-weight: 400;
font-size: 28rpx;
margin-top: 10rpx;
}
.txt1 {
color: #F45E04;
}
.txt2 {
color: #666666;
}
.txt3 {
color: #FF0B0B;
}
.txt4 {
color: #057D51;
}
}
}
.tab-section {
margin: 40rpx 0;
}
.search-input {
.input-content {
background-color: white;
border-radius: 30rpx;
height: 60rpx;
margin-right: 16rpx;
overflow: hidden;
flex: 1;
::v-deep(.picker) {
height: 60rpx;
.picker-cotent{
font-family: Alibaba PuHuiTi;
font-weight: 400;
font-size: 28rpx;
color: #333333;
padding-left: 30rpx;
}
.wd-cell__wrapper {
// padding: 0 !important;
// padding-right: 20rpx !important;
}
}
input {
flex: 1;
height: 50rpx;
border-left: 1px solid #f1f1f1;
padding: 0 20rpx;
}
}
::v-deep(.search-btn) {
button {
width: 130rpx !important;
min-width: 130rpx !important;
height: 60rpx !important;
background: #0478F4;
border-radius: 30rpx;
}
}
}
.carditem {
padding: 30rpx;
margin-top: 20rpx;
.carditem-top {
gap:10rpx;
.time {
font-family: Alibaba PuHuiTi;
font-weight: 400;
font-size: 24rpx;
color: #999999;
flex: 1;
}
[class^='status'] {
font-family: Alibaba PuHuiTi;
font-weight: 400;
font-size: 24rpx;
border-radius: 20rpx;
padding: 5rpx 20rpx;
}
$status-map: (
1: #F45E04,
2: #FF0000,
3:#3F8E0B,
4:#3A43EF,
5:#666666
);
@each $status,
$color in $status-map {
.status#{$status} {
color: $color;
border: 1px solid $color;
}
}
$goodstatus-map: (
6: #0478F4,
7: #FF0B0B
);
@each $status,
$color in $goodstatus-map {
.status#{$status} {
color: white;
background: $color ;
border: 1px solid $color;
}
}
}
.carditem-info {
gap: 5rpx;
.title {
font-family: Alibaba PuHuiTi;
font-weight: 500;
font-size: 28rpx;
color: #333333
}
.label {
font-family: Alibaba PuHuiTi;
font-weight: 400;
font-size: 20rpx;
color: #999999;
line-height: 30rpx;
height: 30rpx;
padding: 0rpx 10rpx;
background: #E7E7E7;
border-radius: 8rpx;
}
}
.carditem-bottom {
background: #F8F8F8;
border-radius: 12rpx;
padding: 25rpx;
margin-top: 25rpx;
.carditem-bottom-item {
font-family: Alibaba PuHuiTi;
font-weight: 400;
font-size: 24rpx;
line-height: 34rpx;
.label {
color: #333333;
}
.val {
color: #666666;
}
}
}
}
</style>

View File

@ -0,0 +1,164 @@
<template>
<view class="content">
<!-- 头部 -->
<view class="content-item">
<view class="content-item-top">
<view class="content-item-top-title">{{data.name}}</view>
<view class="content-item-top-tip">
<text>查看更多</text>
<!-- <image :src="imgUrl"></image> -->
</view>
</view>
<view class="content-date">{{data.date}}</view>
</view>
<!-- 底部自定义内容区域 -->
<view class="content-bottom">
<view class="content-bottom-item">
<text class="content-bottom-item-lable">供应商</text>
<view class="content-bottom-item-value">
<view class="content-bottom-item-value-info">
{{data.company_sell_area}}
</view>
<text>{{data.company_sell}}</text>
</view>
</view>
<view class="content-bottom-item">
<text class="content-bottom-item-lable">采购商</text>
<view class="content-bottom-item-value">
<view class="content-bottom-item-value-info">
{{data.company_buy_area}}
</view>
<text>{{data.company_buy}}</text>
</view>
</view>
<view class="content-bottom-item">
<text class="content-bottom-item-lable">起运港</text>
<text class="content-bottom-item-value">{{data.company_sell_area}}</text>
</view>
<view class="content-bottom-item">
<text class="content-bottom-item-lable">目的港</text>
<text class="content-bottom-item-value">{{data.company_sell_area}}</text>
</view>
<view class="content-bottom-item content-bottom-last">
<text class="content-bottom-item-lable">产品描述</text>
<text class="content-bottom-item-value">{{data.remarks}}</text>
</view>
</view>
</view>
</template>
<script setup>
import {
defineProps
} from 'vue'
//
const props = defineProps({
data: {
type: Object,
default: () => {
return {}
}
}
})
</script>
<style lang="scss" scoped>
.content {
width: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
margin-top: 20rpx;
color: #333333;
font-size: 28rpx;
font-weight: 600;
background: #E9F3FF;
border-radius: 24rpx;
padding: 30rpx;
&-item {
width: 100%;
border-bottom: 1rpx solid #C6E0FF;
height: 120rpx;
&-top {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
&-tip {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 24rpx;
color: #666;
font-weight: 400;
image {
width: 24rpx;
height: 24rpx;
margin-left: 10rpx;
}
}
}
}
&-date {
color: #666;
font-weight: 400;
margin: 10rpx 0;
}
&-bottom {
margin-top: 20rpx;
font-weight: 500;
font-size: 24rpx;
&-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10rpx 0;
&-lable {
color: #666;
}
&-value {
display: flex;
align-items: center;
justify-content: end;
width: 80%;
&-info {
background: #85BCFF;
border-radius: 30rpx;
padding: 0 10rpx;
color: #0052B5;
margin-right: 10rpx;
font-size: 18rpx;
}
}
}
&-last {
align-items: flex-start;
height: 80rpx;
.content-bottom-item-value {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
}
}
}
</style>

View File

@ -0,0 +1,66 @@
<template>
<view class="content">
<!-- 左侧标题区域 -->
<view class="content-left">
<view class="content-bar"></view>
<text>{{ title }}</text>
</view>
<!-- 右侧自定义内容区域 -->
<view class="content-right">
<slot></slot>
</view>
</view>
</template>
<script setup>
import {
defineProps
} from 'vue'
//
const props = defineProps({
title: {
type: String,
required: true, //
default: '' //
}
})
</script>
<style lang="scss" scoped>
.content {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 20rpx;
color: #333333;
font-size: 28rpx;
font-weight: 600;
&-left {
display: flex;
align-items: center;
}
&-right {
display: flex;
align-items: center;
font-size: 24rpx;
color: #666666;
image {
width: 15rpx;
height: 13rpx;
margin-left: 10rpx;
}
}
&-bar {
width: 8rpx;
height: 30rpx;
margin-right: 10rpx;
background: linear-gradient(to bottom, #1975C3, #fff);
}
}
</style>

View File

@ -0,0 +1,390 @@
<template>
<view class="portrait-page" :style="backgroundStyle">
<view :style="topGradientStyle"></view>
<view class="portrait-page-contain">
<!-- 搜索 -->
<wd-search v-model="searchValue" :placeholder-left="placeholderLeft" cancel-txt="搜索" placeholder="请输入企业名称" />
<!--企业卡片 -->
<view class="portrait-card">
<view class="portrait-card-title">
<view class="portrait-card-title-circle"></view>
{{company.company_name}}
</view>
<view class="portrait-card-info">
<view class="portrait-card-info-item">
<view class="portrait-card-info-item-label">联系人</view>
<view class="portrait-card-info-item-value">{{company.company_contact}}</view>
</view>
<view class="portrait-card-info-item">
<view class="portrait-card-info-item-label">联系电话</view>
<view class="portrait-card-info-item-value">{{company.company_mobile}}</view>
</view>
<view class="portrait-card-info-item">
<view class="portrait-card-info-item-label">邮箱</view>
<view class="portrait-card-info-item-value">{{company.company_email}}</view>
</view>
<view class="portrait-card-info-item">
<view class="portrait-card-info-item-label">地址</view>
<view class="portrait-card-info-item-value">{{company.company_address}}</view>
</view>
</view>
</view>
<!-- tab -->
<wd-tabs v-model="tabChange" @change="tabIndex">
<block v-for="(item,index) in tabList" :key="item">
<wd-tab :title="item">
<!-- 贸易数据 -->
<view class="content-trade" v-if="item=='贸易数据'">
<MarketTitle title='市场趋势分析'>
<view class="content-right">
<text>2025</text>
<image :src="imgsUrl.lower_img"></image>
</view>
</MarketTitle>
<view class="content-trade-stat">
<view v-for="(item,index) in tradeData.market_analysis_board" :key="index"
@click="handleStatClick(index)" class="content-trade-stat-item"
:class="{ 'content-trade-stat-item-active': activeIndex === index }">
<text class="content-trade-stat-value">{{item.value}}</text>
<text class="content-trade-stat-label">{{item.name}}</text>
</view>
</view>
<!-- 图表 -->
<qiun-data-charts type="column" :opts="tradeOpts" :chartData="tradeCharts" class="trade-charts" />
<MarketTitle title='近三个月'></MarketTitle>
<wd-table :data="tradeDataList" :stripe="true" rowHeight="10" class="table">
<wd-table-col v-for="(column, index) in tableColumns" :key="index" :prop="column.prop"
:label="column.label" :width="index==0?100:80" align="center" ellipsis="true"></wd-table-col>
</wd-table>
</view>
<!-- 出口数据 -->
<view class="content-trade" v-if="item=='出口数据'">
<MarketTitle title='出口数据分析'>
<view class="content-right">
<wd-search v-model="exportSeach" :placeholder-left="placeholderLeft" cancel-txt=" "
placeholder="请搜索" />
</view>
</MarketTitle>
<block v-for="(item,index) in exportData" :key="index">
<DataCard :data="item"></DataCard>
</block>
</view>
</wd-tab>
</block>
</wd-tabs>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useHomePage } from '@/composables/useHomePage'
import { postCompanyDataThirdDataViewApi, postDataViewExportApi } from '@/api/portrait'
import { onLoad } from '@dcloudio/uni-app'
import MarketTitle from './components/market-title.vue'
import DataCard from './components/data-card.vue'
const {
backgroundStyle,
topGradientStyle
} = useHomePage()
//
const imgsUrl = ref({})
//
const searchValue = ref('')
// tab
const tabChange = ref()
// tabl
const tabList = ref(["贸易数据", "出口数据", "贸易伙伴", "HS编码", "出口产品"])
//
const params = {
page: 1,
page_size: 10
}
//
const tradeData = ref({})
//
const company = ref({})
// -1
const activeIndex = ref(-1);
//
const tradeCharts = ref({
categories: [],
series: []
})
// opt
const tradeOpts = ref({
color: ["#A2D2FF"],
padding: [15, 15, 0, 5],
dataLabel: false,//
legend: {
show: false
},
extra: {
column: {
type: "group",
width: 23,
activeBgColor: "#000000",
seriesGap: 5,
linearOpacity: 1,
barBorderCircle: true
},
},
yAxis: {
data: [
{
min: 0,
axisLine: false
}
]
}
})
//
const tradeDataList = ref([])
//
const tableColumns = ref([])
//
const exportSeach = ref('')
//
const exportData = ref({})
//
const placeholderLeft = ref(true)
onLoad(() => {
getCompanyDataThirdDataView()
getDataViewExport()
})
// tab
const tabIndex = (index) => {
if (index == 1) {
getDataViewExport()
}
}
//
const getCompanyDataThirdDataView = () => {
postCompanyDataThirdDataViewApi(params).then(res => {
if (res.code == 1) {
tradeData.value = res.data
//
imgsUrl.value = res.data.other
//
company.value = res.data.company
tradeCharts.value.categories = res.data.market_analysis_charts.name
let charts = res.data.market_analysis_charts.charts
let chartsObj = {
name: res.data.market_analysis_charts.title,
data: res.data.market_analysis_charts.value
}
tradeCharts.value.series[0] = chartsObj
//
let originalData = res.data.market_analysis_list
tradeDataList.value = originalData.list.map(item => {
return {
[originalData.name[0]]: item[0], //
[originalData.name[1]]: item[1], //
[originalData.name[2]]: item[2], //
[originalData.name[3]]: item[3] //
};
})
extractTableColumns(tradeDataList.value)
}
})
}
//
const handleStatClick = (index) => {
activeIndex.value = index;
}
//
const extractTableColumns = (data) => {
if (data.length > 0) {
//
const keys = Object.keys(data[0]);
//
tableColumns.value = keys.map(key => ({
prop: key,
// label
label: key
}))
}
}
//
const getDataViewExport = () => {
postDataViewExportApi(params).then(res => {
if (res.code == 1) {
exportData.value = res.data
}
})
}
</script>
<style lang="scss" scoped>
::v-deep .portrait-page {
width: 100%;
position: relative;
min-height: 100vh;
overflow-y: hidden;
padding: 0 30rpx;
// 60rpx-100rpx
padding-bottom: 60rpx;
background: #FFFFFF !important;
&-contain {
position: relative;
z-index: 1;
padding-top: 32rpx;
}
}
//
.wd-search {
background-color: #E9F3FF;
height: 80rpx;
border-radius: 24rpx;
}
::v-deep .wd-search__block {
background-color: #E9F3FF;
}
::v-deep .wd-search__cancel {
color: #0478F4;
font-size: 28rpx;
}
//
.portrait-card {
width: 100%;
height: 280rpx;
margin-top: 30rpx;
background: linear-gradient(to bottom, #1975C3, #85BCFF);
border-radius: 20rpx;
padding: 30rpx;
color: #fff;
&-title {
display: flex;
align-items: center;
font-size: 28rpx;
font-weight: 600;
&-circle {
width: 30rpx;
height: 30rpx;
background: #FFFFFF;
border-radius: 50%;
margin-right: 10rpx;
}
}
&-info {
width: 100%;
display: flex;
flex-direction: column;
justify-content: space-around;
padding-bottom: 30rpx;
&-item {
display: flex;
align-items: center;
font-size: 24rpx;
margin-top: 10rpx;
&-label {
width: 120rpx;
}
&-value {
flex: 1;
text-align: right;
}
}
}
}
::v-deep .is-active {
color: #0478F4;
}
.content-right {
display: flex;
align-items: center;
font-size: 24rpx;
color: #666666;
image {
width: 15rpx;
height: 13rpx;
margin-left: 10rpx;
}
.wd-search {
width: 400rpx;
height: 50rpx;
}
::v-deep .wd-search__cover {
background: none;
box-sizing: border-box;
}
::v-deep .wd-search__input {
height: 20rpx;
}
}
.content-trade {
&-stat {
display: flex;
justify-content: space-between;
margin: 30rpx 0;
&-item {
width: 200rpx;
height: 100rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 24rpx;
color: #333333;
background-color: #EEEEEE;
border-radius: 10px;
}
&-item-active {
background-color: #B9C3FF;
color: #1159C0;
border: 1px solid #1159C0;
}
&-value {
font-size: 28rpx;
font-weight: 600;
}
}
}
.trade-charts {
height: 360rpx;
}
::v-deep .is-border {
border-right: none !important;
}
::v-deep .wd-table__cell {
background-color: #F8F8F8 !important;
min-height: 80rpx !important;
}
::v-deep .is-stripe {
background-color: #CCE6FF !important;
}
.table {
width: 100%;
margin-top: 30rpx;
}
</style>

View File

@ -0,0 +1,796 @@
<template>
<view class="container">
<view class="top-bg"></view>
<view class="top">
<view class="top-left">
<wd-icon name="arrow-left" size="22px"></wd-icon>
</view>
<view class="top-title">
客户管理
</view>
</view>
<view class="tab-bar">
<view
v-for="(tab, index) in tabList"
:key="tab.id"
class="tab-item"
:class="{ active: activeTab === index }"
@click="handleTabClick(index)"
>
{{ tab.name }}
</view>
</view>
<view class="content">
<!-- 客户列表 -->
<view v-if="activeTab === 0" class="list">
<view class="search">
<wd-search :placeholder-left="true" hide-cancel light class="search-box" />
<view class="right">
<view class="item">
排序
<image
src="https://apis.haibao.shop/storage/config/20250826/Polygon22x096dd60f7ca5e4e1ec14a75787200d19556225e7.jpg"
mode="widthFix"></image>
</view>
<view class="item" style="margin-left: 44rpx;">
筛选
<image
src="https://apis.haibao.shop/storage/config/20250826/Polygon22x096dd60f7ca5e4e1ec14a75787200d19556225e7.jpg"
mode="widthFix"></image>
</view>
</view>
</view>
<view class="list-item">
<view class="item-top">
<view class="item-top-left">
<view class="avatar">
</view>
<view class="name">
空气猫有限公司
</view>
</view>
<view class="item-top-right">
<image
src="https://apis.haibao.shop/storage/config/20250826/Vector2xedb375099c44a935a7add35fdb9901e086191475.png"
class="phone" mode="widthFix"></image>
<image
src="https://apis.haibao.shop/storage/config/20250826/更多12x8a0b1bca93e23a53d85843c64f375d82efa4199b.png"
class="more" mode="widthFix"></image>
</view>
</view>
<view class="item-content">
<view class="item-content-left">
<view class="line">
<view class="label">
最后跟进
</view>
<view class="date">
2025-08023 12:22
</view>
</view>
<view class="line">
<view class="label">
客户星级
</view>
<view>
<image class="icon"
src="https://apis.haibao.shop/storage/config/20250826/Component302xad7d6290c98da23282c2a64713987e2456827bad.png"
mode="widthFix"></image>
<image class="icon"
src="https://apis.haibao.shop/storage/config/20250826/Component302xad7d6290c98da23282c2a64713987e2456827bad.png"
mode="widthFix"></image>
<image class="icon"
src="https://apis.haibao.shop/storage/config/20250826/Component302xad7d6290c98da23282c2a64713987e2456827bad.png"
mode="widthFix"></image>
<image class="icon"
src="https://apis.haibao.shop/storage/config/20250826/Component302xad7d6290c98da23282c2a64713987e2456827bad.png"
mode="widthFix"></image>
<image class="icon"
src="https://apis.haibao.shop/storage/config/20250826/Component302xad7d6290c98da23282c2a64713987e2456827bad.png"
mode="widthFix"></image>
</view>
</view>
</view>
<view class="item-content-right">
正在进行
</view>
</view>
<view class="item-bottom">
<view class="btn-left">
跟进
</view>
<view class="btn-right">
任务
</view>
</view>
</view>
</view>
<!-- 新建客户 -->
<view v-else-if="activeTab === 1" class="create-customer">
<view class="form-container">
<!-- 基本信息标题 -->
<view class="section-title">基本信息</view>
<!-- 企业名称 -->
<view class="form-item">
<view class="form-label required">企业名称</view>
<wd-input
v-model="customerForm.companyName"
placeholder="请输入企业名称"
class="form-input"
/>
</view>
<!-- 选择行业 -->
<view class="form-item">
<view class="form-label">选择行业</view>
<wd-picker
:columns="industryOptions"
v-model="customerForm.industry"
placeholder="请选择行业"
class="form-picker"
/>
</view>
<!-- 国家地区 -->
<view class="form-item">
<view class="form-label">国家地区</view>
<wd-picker
:columns="countryOptions"
v-model="customerForm.country"
placeholder="请选择国家"
class="form-picker"
/>
</view>
<!-- 企业地址 -->
<view class="form-item">
<view class="form-label">企业地址</view>
<wd-picker
:columns="addressOptions"
v-model="customerForm.address"
placeholder="请选择企业地址"
class="form-picker"
/>
</view>
<!-- 营销阶段 -->
<view class="form-item">
<view class="form-label">营销阶段</view>
<wd-picker
:columns="marketingStageOptions"
v-model="customerForm.marketingStage"
placeholder="请选择营销阶段"
class="form-picker"
/>
</view>
<!-- 标签 -->
<view class="form-item">
<view class="form-label">标签</view>
<wd-picker
:columns="tagOptions"
v-model="customerForm.tags"
placeholder="请选择标签"
class="form-picker"
/>
</view>
</view>
</view>
<!-- 联系人基本信息 -->
<view v-if="activeTab === 1" class="create-customer" style="margin-top: 20rpx;">
<view class="form-container">
<!-- 基本信息标题 -->
<view class="section-title">基本信息</view>
<!-- 联系人姓名和性别 -->
<view class="form-item">
<view class="name-gender-row">
<view class="name-section">
<view class="form-label">联系人姓名</view>
<wd-input
v-model="customerForm.contactName"
placeholder="请输入联系人姓名"
class="form-input"
/>
</view>
<view class="gender-section">
<view class="gender-label">性别</view>
<wd-picker
:columns="genderOptions"
v-model="customerForm.gender"
placeholder="请选择性别"
class="form-picker"
/>
</view>
</view>
</view>
<!-- 联系电话 -->
<view class="form-item">
<view class="form-label">联系电话</view>
<wd-input
v-model="customerForm.phone"
type="number"
placeholder="请输入联系电话"
class="form-input"
/>
</view>
<!-- 邮箱 -->
<view class="form-item">
<view class="form-label">邮箱</view>
<wd-input
v-model="customerForm.email"
placeholder="请输入邮箱"
class="form-input"
/>
</view>
<!-- 职位 -->
<view class="form-item">
<view class="form-label">职位</view>
<wd-input
v-model="customerForm.position"
placeholder="请输入职位"
class="form-input"
/>
</view>
<!-- 备注 -->
<view class="form-item">
<view class="form-label">备注</view>
<wd-input
v-model="customerForm.remarks"
placeholder="请输入备注"
class="form-input"
/>
</view>
<!-- 提交按钮 -->
<view class="submit-btn-container">
<wd-button
type="primary"
block
@click="submitCustomer"
class="submit-btn"
>
保存客户
</wd-button>
</view>
</view>
</view>
<!-- 数据看板 -->
<view v-else-if="activeTab === 2" class="dashboard">
<view class="placeholder-content">
<text>数据看板页面内容</text>
</view>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
// tab
const activeTab = ref(0)
// tab
const tabList = ref([
{
id: 0,
name: '客户列表'
},
{
id: 1,
name: '新建客户'
},
{
id: 2,
name: '数据看板'
}
])
//
const customerForm = ref({
companyName: '',
industry: '',
country: '',
address: '',
marketingStage: '',
tags: '',
contactName: '',
gender: '',
phone: '',
email: '',
position: '',
remarks: ''
})
//
const industryOptions = ref(['制造业', '服务业', '科技行业', '金融业', '教育行业', '医疗行业', '其他'])
const countryOptions = ref(['中国', '美国', '英国', '德国', '日本', '韩国', '其他'])
const addressOptions = ref(['北京市', '上海市', '广州市', '深圳市', '杭州市', '成都市', '其他'])
const marketingStageOptions = ref(['潜在客户', '意向客户', '商机客户', '成交客户', '流失客户'])
const tagOptions = ref(['重要客户', '优质客户', '普通客户', '新客户', '老客户'])
const genderOptions = ref(['男', '女'])
// tab
const handleTabClick = (index: number) => {
activeTab.value = index
}
//
const submitCustomer = () => {
//
if (!customerForm.value.companyName.trim()) {
uni.showToast({
title: '请输入企业名称',
icon: 'none'
})
return
}
if (!customerForm.value.contactName.trim()) {
uni.showToast({
title: '请输入联系人姓名',
icon: 'none'
})
return
}
if (!customerForm.value.phone.trim()) {
uni.showToast({
title: '请输入联系电话',
icon: 'none'
})
return
}
//
const phoneRegex = /^1[3-9]\d{9}$/
if (!phoneRegex.test(customerForm.value.phone)) {
uni.showToast({
title: '请输入正确的手机号',
icon: 'none'
})
return
}
//
if (customerForm.value.email && customerForm.value.email.trim()) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(customerForm.value.email)) {
uni.showToast({
title: '请输入正确的邮箱格式',
icon: 'none'
})
return
}
}
//
uni.showLoading({
title: '保存中...'
})
//
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '保存成功',
icon: 'success'
})
//
customerForm.value = {
companyName: '',
industry: '',
country: '',
address: '',
marketingStage: '',
tags: '',
contactName: '',
gender: '',
phone: '',
email: '',
position: '',
remarks: ''
}
// tab
setTimeout(() => {
activeTab.value = 0
}, 1500)
}, 1500)
}
</script>
<style lang="scss" scoped>
.active {
color: #333333 !important;
&::after {
content: "";
display: block;
width: 35rpx;
height: 6rpx;
background: #333333;
border-radius: 20rpx 20rpx 20rpx 20rpx;
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
}
}
.container {
overflow: hidden;
background: #FAFAFA;
}
.top-bg {
width: 750rpx;
height: 513rpx;
background: linear-gradient(134deg, #E8FFDE 0%, #BCF3FF 100%);
border-radius: 50rpx 50rpx 50rpx 50rpx;
position: absolute;
}
.top {
height: 176rpx;
display: flex;
align-items: flex-end;
padding-bottom: 20rpx;
box-sizing: content-box;
position: relative;
.top-left {
height: 88rpx;
display: flex;
align-items: center;
padding-left: 33rpx;
}
.top-title {
font-size: 32rpx;
height: 88rpx;
display: flex;
align-items: center;
position: absolute;
left: 50%;
transform: translateX(-50%);
}
}
.tab-bar {
padding: 0 33rpx;
display: flex;
.tab-item {
flex: 1;
display: flex;
justify-content: center;
padding: 10rpx 0;
font-size: 28rpx;
color: #9CA9AA;
position: relative;
}
}
.content {
padding: 0 33rpx;
margin-top: 27rpx;
position: relative;
z-index: 1;
}
.create-customer {
padding: 30rpx;
box-shadow: 0rpx 1rpx 20rpx 0rpx rgba(177, 211, 255, 0.15);
border-radius: 12rpx 12rpx 12rpx 12rpx;
background: #FFFFFF;
.form-container {
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333333;
margin-bottom: 30rpx;
}
.form-item {
margin-bottom: 30rpx;
.form-label {
font-size: 28rpx;
color: #333333;
margin-bottom: 16rpx;
position: relative;
&.required::before {
content: "*";
color: #ff4757;
margin-right: 8rpx;
}
}
.form-input {
background: #F8F8F8 !important;
border-radius: 12rpx;
padding: 10rpx 31rpx;
font-size: 24rpx;
border: none !important;
&:focus {
border-color: #4398FF;
}
}
.form-picker {
background: #F8F8F8;
border-radius: 12rpx;
font-size: 28rpx;
border: none;
color: #333333;
min-height: 88rpx;
display: flex;
align-items: center;
}
.name-gender-row {
display: flex;
align-items: flex-start;
gap: 20rpx;
.name-section {
flex: 1;
display: flex;
flex-direction: column;
.form-label {
font-size: 28rpx;
color: #333333;
margin-bottom: 16rpx;
}
}
.gender-section {
flex: 1;
display: flex;
flex-direction: column;
.gender-label {
font-size: 28rpx;
color: #333333;
margin-bottom: 16rpx;
}
}
}
}
.submit-btn-container {
margin-top: 60rpx;
.submit-btn {
background: #4398FF;
border-radius: 50rpx;
height: 88rpx;
font-size: 32rpx;
font-weight: bold;
}
}
}
//
::v-deep .wd-input {
background: #F8F9FA !important;
border-radius: 12rpx !important;
border: 1rpx solid #E9ECEF !important;
font-size: 28rpx !important;
}
//
::v-deep .wd-input.is-not-empty:not(.is-disabled)::after {
background-color: transparent !important;
}
::v-deep .wd-input::after {
background-color: transparent !important;
}
::v-deep .wd-picker {
background: #F8F9FA !important;
border-radius: 12rpx !important;
border: none !important;
font-size: 28rpx !important;
min-height: 88rpx !important;
}
// picker
::v-deep .wd-picker__inner {
border: none !important;
background: #F8F9FA !important;
}
::v-deep .wd-picker__cell {
border: none !important;
background: #F8F9FA !important;
}
::v-deep .wd-picker__placeholder {
color: #999999 !important;
}
::v-deep .wd-button--primary {
background: #4398FF !important;
border-color: #4398FF !important;
}
}
.dashboard {
padding: 30rpx;
box-shadow: 0rpx 1rpx 20rpx 0rpx rgba(177, 211, 255, 0.15);
border-radius: 12rpx 12rpx 12rpx 12rpx;
background: #FFFFFF;
min-height: 400rpx;
display: flex;
align-items: center;
justify-content: center;
.placeholder-content {
text-align: center;
color: #999999;
font-size: 28rpx;
}
}
.search {
display: flex;
// align-items: center;
justify-content: space-between;
.search-box {
width: 60%;
background-color: #fff !important;
padding: 0 !important;
border-radius: 40rpx;
border: 1rpx solid #62DF91;
}
.right {
display: flex;
align-items: center;
.item {
font-size: 28rpx;
color: #333333;
display: flex;
align-items: center;
image {
width: 23rpx;
margin-left: 4rpx;
}
}
}
}
.list {
padding: 30rpx;
box-shadow: 0rpx 1rpx 20rpx 0rpx rgba(177, 211, 255, 0.15);
border-radius: 12rpx 12rpx 12rpx 12rpx;
background: #FFFFFF;
.list-item {
padding: 30rpx 0;
border-bottom: 1rpx solid #E9E9E9;
.item-top {
display: flex;
justify-content: space-between;
align-items: center;
.item-top-left {
display: flex;
align-items: center;
.avatar {
width: 38rpx;
height: 38rpx;
background: #D9D9D9;
border-radius: 50%;
margin-right: 8rpx;
}
.name {
font-size: 28rpx;
font-weight: bold;
color: #333333;
}
}
.item-top-right {
.phone {
width: 23rpx;
margin-right: 11rpx;
}
.more {
width: 26rpx;
}
}
}
.item-content {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin: 10rpx 0 30rpx;
.item-content-left {
.line {
display: flex;
align-items: center;
line-height: 44rpx;
font-size: 24rpx;
.label {
color: #999999;
}
.date {
color: #333333;
}
.icon {
width: 24rpx;
margin-right: 20rpx;
}
}
}
.item-content-right {
padding: 10rpx 30rpx;
border-radius: 30rpx;
background: #D9FFDC;
border-radius: 30rpx 30rpx 30rpx 30rpx;
font-size: 28rpx;
color: #00BE33;
font-weight: bold;
}
}
.item-bottom {
display: flex;
.btn {
flex: 1;
border-radius: 50rpx;
padding: 18rpx 0;
font-size: 32rpx;
text-align: center
}
.btn-left {
@extend .btn;
margin-right: 13rpx;
color: #FFFFFF;
background: #4398FF;
}
.btn-right {
@extend .btn;
margin-left: 13rpx;
color: #4398FF;
border: 2rpx solid #4398FF;
}
}
}
}
</style>

View File

@ -0,0 +1,266 @@
<template>
<view class="customs-service-grid" v-if="configData">
<!-- 顶部统计栏 -->
<view class="stats-bar customs-service-grid-nav">
<view class="stat-item" v-for="(item,index) in configData.board" :key="index">
<text class="stat-number">{{item.value}}</text>
<text class="stat-label">{{item.title}}</text>
</view>
</view>
<view class="service-section-container">
<!-- 我要办 -->
<view class="service-section">
<view class="section-header">
<section-title title="我要办" />
</view>
<view class="service-grid">
<view v-for="(service, index) in configData.menusDo" :key="index" class="service-item"
@click="handleServiceClick(service.url)">
<view class="service-icon">
<image :src="service.img" class="img"></image>
</view>
<text class="service-title">{{ service.name }}</text>
</view>
</view>
</view>
<!-- 我要查 -->
<view class="service-section">
<view class="section-header">
<section-title title="我要查" />
</view>
<view class="service-grid">
<view v-for="(service, index) in configData.menusSearch" :key="index" class="service-item"
@click="handleServiceClick(service.url)">
<view class="service-icon">
<image :src="service.img" class="img"></image>
</view>
<text class="service-title">{{ service.name }}</text>
</view>
</view>
</view>
<!-- 公共服务 -->
<view class="service-section">
<view class="section-header">
<section-title title="电子账册" @title-click="handleTitleClick" />
</view>
<view class="service-grid">
<view
v-for="(service, index) in configData.menusCustomsBook"
:key="index"
class="service-item"
@click="handleServiceClick(service)"
>
<view class="service-icon" >
<image :src="service.img" class="img"></image>
</view>
<text class="service-title">{{ service.name }}</text>
</view>
</view>
</view>
<!-- 展转贸 -->
<view class="service-section">
<view class="section-header">
<section-title title="展转贸" />
</view>
<view class="service-grid">
<view v-for="(service, index) in configData.menusExpoToTrade" :key="index" class="service-item"
@click="handleServiceClick(service.url)">
<view class="service-icon">
<image :src="service.img" class="img"></image>
</view>
<text class="service-title">{{ service.name }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { postConfigApi } from '@/api/customs'
interface ServiceItem {
id : string
title : string
icon : string
color : string
route ?: string
}
const handleServiceClick = (url : string) => {
console.log('点击服务:', url)
if (url && url != '#') {
uni.navigateTo({
url,
fail: () => {
uni.reLaunch({
url
})
}
})
}
//
// uni.showToast({
// title: `${service.title}`,
// icon: 'none',
// })
}
const configData = ref(null)
const getList = () => {
postConfigApi({
page: 1,
page_size: 10
}).then(res => {
if (res.code == 1) {
configData.value = res.data
}
})
}
getList()
</script>
<style lang="scss" scoped>
.customs-service-grid {
position: relative;
z-index: 2;
padding: 0 32rpx;
}
.customs-service-grid-nav {
display: flex;
justify-content: space-around;
align-items: center;
}
//
.stats-bar {
position: relative;
z-index: 2;
background: #ffffff;
border-radius: 20rpx;
padding: 40rpx 32rpx;
// margin-bottom: 40rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
.stat-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
.stat-number {
font-size: 48rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
}
.stat-label {
font-size: 24rpx;
color: #666;
text-align: center;
}
}
}
.service-section-container {
background: #ffffff;
position: absolute;
left: 0;
width: 100%;
// border-radius: 80rpx 80rpx 0 0;
&::before {
content: '';
position: absolute;
z-index: 1;
top: -100rpx;
left: 0;
right: 0;
height: 100rpx;
background: #ffffff;
border-radius: 80rpx 80rpx 0 0;
}
}
//
.service-section {
padding: 32rpx;
// margin-bottom: 32rpx;
.section-header {
display: flex;
align-items: center;
margin-bottom: 32rpx;
.section-bar {
width: 8rpx;
height: 32rpx;
background: #4a90e2;
border-radius: 4rpx;
margin-right: 16rpx;
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
}
.service-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 24rpx;
.service-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
.service-icon {
width: 80rpx;
height: 80rpx;
// border: 3rpx dashed;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.8);
transition: all 0.3s ease;
.img {
width: 50rpx;
height: 50rpx;
}
.icon-text {
font-size: 32rpx;
}
}
.service-title {
font-size: 24rpx;
color: #333;
text-align: center;
line-height: 1.2;
max-width: 100%;
word-break: break-all;
}
&:active .service-icon {
transform: scale(0.95);
opacity: 0.8;
}
}
}
}
</style>

View File

@ -0,0 +1,68 @@
<template>
<view class="customs-express-page" :style="backgroundStyle">
<!-- 顶部渐变背景 -->
<view :style="topGradientStyle"></view>
<!-- 状态栏占位 -->
<view class="status-bar"></view>
<!-- 服务网格 -->
<customs-service-grid />
</view>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'
import { useCustomsExpress } from '@/composables/useCustomsExpress'
import CustomsServiceGrid from './components/customs-service-grid.vue'
// 使
const { backgroundStyle, topGradientStyle } = useCustomsExpress()
//
onMounted(() => {
//
uni.setNavigationBarTitle({
title: '海关服务',
})
})
onUnmounted(() => {})
</script>
<style lang="scss" scoped>
.customs-express-page {
position: relative;
min-height: 100vh;
padding: 0;
padding-bottom: 60rpx;
}
.status-bar {
height: 88rpx;
background: transparent;
position: relative;
z-index: 2;
}
.page-header {
position: relative;
z-index: 2;
text-align: center;
padding: 40rpx 32rpx 60rpx;
.page-title {
display: block;
font-size: 48rpx;
font-weight: bold;
color: #333;
margin-bottom: 16rpx;
}
.page-subtitle {
display: block;
font-size: 28rpx;
color: #666;
}
}
</style>

View File

@ -0,0 +1,258 @@
<template>
<view class="smart_body_box" v-if="dataConfig">
<view class="header_card_box">
<view class="item_num_box" v-for="(item,index) in dataConfig.board" :key="index">
<text class="num_ti">{{item.value}}</text>
<view class="lab_ti">{{item.title}}</view>
</view>
</view>
<view class="menu_body_box">
<view class="menu_label_box">
<text class="la_ti">海保服务</text>
<view class="more_box" @click="changeMore('menus')" v-if="menus.length > 4">
更多
<wd-icon name="arrow-down" size="12px" color="#3F8C8B" class="ml-3 "
:class="showOption.menus ?'more_up' : 'more_down'"></wd-icon>
</view>
</view>
<view class="menu_icon_box" :style="{height: showOption.menus ? 'auto': '100px'}">
<view class="menu_item_box " v-for="(item,index) in dataConfig.menus" :key="index" @click="jumpUrl(item.url)">
<image :src="item.img" class="menu_img"></image>
<view class="title_box">{{item.name}}</view>
</view>
</view>
</view>
<view class="menu_body_box">
<view class="menu_label_box">
<text class="la_ti">物流跟踪</text>
<view class="more_box" @click="changeMore('menusLogisticsTracking')" v-if="menusLogisticsTracking.length > 4">
更多
<wd-icon name="arrow-down" size="12px" color="#3F8C8B" class="ml-3"
:class="showOption.menusLogisticsTracking ?'more_up' : 'more_down'"></wd-icon>
</view>
</view>
<view class="menu_icon_box" :style="{height: showOption.menusLogisticsTracking ? 'auto': '100px'}">
<view class="menu_item_box " @click="jumpUrl(item.url)"
v-for="(item,index) in dataConfig.menusLogisticsTracking" key="index">
<image :src="item.img" class="menu_img"></image>
<view class="title_box">{{item.name}}</view>
</view>
</view>
</view>
<view class="menu_body_box">
<view class="menu_label_box">
<text class="la_ti">查询工具</text>
<view class="more_box" @click="changeMore('menusSearchTool')" v-if="menusSearchTool.length > 4">
更多
<wd-icon :class="showOption.menusSearchTool ?'more_up' : 'more_down'" name="arrow-down" size="12px"
color="#3F8C8B" class="ml-3"></wd-icon>
</view>
</view>
<view class="menu_icon_box" :style="{height: showOption.menusSearchTool ? 'auto': '100px'}">
<view class="menu_item_box " @click="jumpUrl(item.url)" v-for="(item,index) in dataConfig.menusSearchTool">
<image :src="item.img" class="menu_img"></image>
<view class="title_box">{{item.name}}</view>
</view>
</view>
</view>
<view class="menu_body_box">
<view class="menu_label_box">
<text class="la_ti">增值服务</text>
<view class="more_box" @click="changeMore('menusAddedServices')" v-if="menusAddedServices.length > 4">
更多
<wd-icon :class="showOption.menusAddedServices ?'more_up' : 'more_down'" name="arrow-down" size="12px"
color="#3F8C8B" class="ml-3"></wd-icon>
</view>
</view>
<view class="menu_icon_box" :style="{height: showOption.menusAddedServices ? 'auto': '100px'}">
<view class="menu_item_box " @click="jumpUrl(item.url)" v-for="(item,index) in dataConfig.menusAddedServices">
<image :src="item.img" class="menu_img"></image>
<view class="title_box">{{item.name}}</view>
</view>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { postDataSmartApi } from '@/api/customs'
const dataConfig = ref({})
const showOption = reactive({
menusHbsm: false,
menusLogisticsTracking: false,
menusSearchTool: false,
menusAddedServices: false
})
//
const menus = ref([])
const menusAddedServices = ref([])
const menusLogisticsTracking = ref([])
const menusSearchTool = ref([])
const params = {
page: 1,
page_size: 10,
}
onLoad(async () => {
await getData()
})
const getData = () => {
postDataSmartApi(params).then(res => {
if (res.code == 1) {
dataConfig.value = res.data
menus.value = res.data.menus
menusAddedServices.value = res.data.menusAddedServices
menusLogisticsTracking.value = res.data.menusLogisticsTracking
menusSearchTool.value = res.data.menusSearchTool
}
})
}
const jumpUrl = (url) => {
if (url && url != '#') {
uni.navigateTo({
url,
fail: () => {
uni.reLaunch({
url
})
}
})
}
}
const changeMore = (data) => {
showOption[data] = !showOption[data]
}
</script>
<style>
page {
height: 100vh;
background: #F9FAFB;
}
</style>
<style lang="scss" scoped>
.smart_body_box {
width: 100%;
padding: 0 16px;
box-sizing: border-box;
.menu_body_box {
width: 100%;
background: #FFFFFF;
padding: 40rpx 0;
box-sizing: border-box;
margin-top: 14px;
border-radius: 6px;
.menu_icon_box {
width: 100%;
display: flex;
align-items: center;
flex-wrap: wrap;
overflow: hidden;
.menu_item_box {
width: calc(100% / 4);
display: flex;
flex-direction: column;
align-items: center;
align-items: center;
justify-content: center;
margin-top: 18px;
.title_box {
width: 56px;
height: 38px;
text-align: center;
margin-top: 6px;
font-size: 14px;
color: #333333;
}
.menu_img {
width: 48px;
height: 48px;
}
}
}
.menu_label_box {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 18px;
box-sizing: border-box;
.more_box {
display: flex;
align-items: center;
font-size: 12px;
color: #3F8C8B;
transition: all .3s linear;
.more_up {
transform: rotate(-180deg);
transition: all .3s linear;
}
.more_down {
transform: rotate(0deg);
transition: all .3s linear;
}
}
.la_ti {
font-size: 16px;
color: #333333;
font-weight: 700 !important;
}
}
}
.header_card_box {
width: 100%;
margin-top: 16px;
background: linear-gradient(47deg, #45908F 0%, #338382 100%);
border-radius: 6px;
padding: 11px 30px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
.item_num_box {
width: 90px;
height: 102px;
background: rgba(255, 255, 255, 0.1);
box-shadow: 0px 0px 4px 0px rgba(35, 83, 83, 0.2);
border-radius: 4px;
border: 1px solid;
border-image: linear-gradient(137deg, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0)) 1 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.lab_ti {
width: 48px;
height: 38px;
text-align: center;
color: #D2EDED;
font-size: 12px;
font-weight: 500;
margin-top: 10rpx;
}
.num_ti {
font-size: 28px;
color: #FFFFFF;
font-weight: 700 !important;
}
}
}
}
</style>

View File

@ -1,214 +0,0 @@
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { changeBindApi } from '@/api/member'
import { refreshPage, uuid } from '@/utils/common'
import { smsTemplateCode } from '@/utils/constants'
import { useMemberStore } from '@/stores'
import { verificationApi } from '@/api/member'
const props = defineProps({
phone: String,
})
const popup = ref()
const popupDialog = ref()
const formRef = ref()
const confirmPopup = ref()
const confirmPopupDialog = ref()
const confrimFormRef = ref()
const profileData = useMemberStore().profile
//
const form = reactive({
password: '',
sms_captcha_code: '',
phone: props.phone,
})
const formRules = {
password: {
rules: [{ required: true, errorMessage: '请输入密码' }],
},
sms_captcha_code: {
rules: [{ required: true, errorMessage: '请输入验证码' }],
},
}
//
const confirmForm = reactive({
phone: '',
sms_captcha_code: '',
verification_token: '',
})
const confirmFormRules = {
phone: {
rules: [{ required: true, errorMessage: '请输入手机号' }],
},
sms_captcha_code: {
rules: [{ required: true, errorMessage: '请输入验证码' }],
},
}
const open = () => {
popup.value?.open()
}
const onConfirm = async () => {
try {
await formRef.value.validate()
const res = await verificationApi({
password: form.password,
sms_captcha_code: form.sms_captcha_code,
})
if (!res.result.verificationToken) {
return uni.showToast({
title: '验证码错误',
icon: 'none',
})
}
confirmForm.verification_token = res.result.verificationToken
popup.value?.close()
confirmPopup.value.open()
} catch (err) {
console.log(err)
}
}
const onConfirmChange = async () => {
try {
await confrimFormRef.value.validate()
const res = changeBindApi({
verification_token: confirmForm.verification_token,
phone: confirmForm.phone,
sms_captcha_code: confirmForm.sms_captcha_code,
})
uni.showToast({
title: '修改成功',
})
setTimeout(() => {
refreshPage()
}, 1500)
} catch (err) {
console.log(err)
}
}
const onBeforeSend = () => {
if (!form.password) {
uni.showToast({
title: '请输入密码',
icon: 'none',
})
return false
}
return true
}
defineExpose({ popup, open })
</script>
<template>
<view>
<!-- 验证框 -->
<uni-popup ref="popup" type="dialog">
<uni-popup-dialog
ref="popupDialog"
mode="input"
title="验证账户信息"
@confirm="onConfirm"
@close="popupDialog.popup.close()"
:before-close="true"
>
<uni-forms
ref="formRef"
:model="form"
:rules="formRules"
validateTrigger="blur"
label-position="top"
label-width="100%"
>
<uni-forms-item required label="用户密码" name="password">
<uni-easyinput
v-model="form.password"
type="password"
placeholder="请输入当前账户密码"
/>
</uni-forms-item>
<shop-sms-captcha
:phone="form.phone"
:template_code="smsTemplateCode.verifyPhone"
:password="form.password"
v-model:sms_captcha="form.sms_captcha_code"
:options="{
label: `短信验证码(接收手机号: ${profileData?.mobile})`,
required: true,
}"
:before-send="onBeforeSend"
/>
</uni-forms>
</uni-popup-dialog>
</uni-popup>
<!-- 修改框 -->
<uni-popup ref="confirmPopup" type="dialog">
<uni-popup-dialog
ref="confirmPopupDialog"
mode="input"
title="修改手机号码"
@confirm="onConfirmChange"
@close="confirmPopupDialog.popup.close()"
:before-close="true"
>
<uni-forms
ref="confrimFormRef"
:model="confirmForm"
:rules="confirmFormRules"
validateTrigger="blur"
label-position="top"
label-width="100%"
>
<uni-forms-item required name="phone" label="新手机号">
<uni-easyinput
v-model="confirmForm.phone"
placeholder="请输入新的手机号"
maxlength="11"
/>
</uni-forms-item>
<shop-sms-captcha
:phone="confirmForm.phone"
:template_code="smsTemplateCode.changePhone"
v-model:sms_captcha="confirmForm.sms_captcha_code"
:options="{
label: '短信验证码',
required: true,
}"
/>
</uni-forms>
</uni-popup-dialog>
</uni-popup>
</view>
</template>
<style lang="scss" scoped>
.remark {
font-size: 24rpx;
padding-bottom: 40rpx;
}
.reset-btn {
color: red;
font-size: 30rpx;
padding-left: 20rpx;
background: #fff;
&::after {
border: none;
}
}
</style>

View File

@ -1,174 +0,0 @@
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { changePasswordApi } from '@/api/member'
import { refreshPage } from '@/utils/common'
const popup = ref()
const popupDialog = ref()
const formRef = ref()
const resetPasswordRef = ref()
const form = reactive({
old_password: '',
new_password: '',
confrim_password: '',
})
const formRules = reactive({
old_password: {
rules: [
{
required: true,
errorMessage: '请输入旧密码',
},
{
validateFunction: (rule: any, value: string, data: any, callback: Function) => {
if (!/^(?!.*[&<>"'\n\r]).{6,32}$/.test(value)) {
return callback('密码要求6到32位不能包含& < > " ')
}
},
},
],
},
new_password: {
rules: [
{
required: true,
errorMessage: '请输入新密码',
},
{
validateFunction: (rule: any, value: string, data: any, callback: Function) => {
if (!/^(?!.*[&<>"'\n\r]).{6,32}$/.test(value)) {
return callback('密码要求6到32位不能包含& < > " ')
}
},
},
],
},
confrim_password: {
rules: [
{
required: true,
errorMessage: '请再次输入新密码',
},
{
validateFunction: (rule: any, value: string, data: any, callback: Function) => {
if (!/^(?!.*[&<>"'\n\r]).{6,32}$/.test(value)) {
return callback('密码要求6到32位不能包含& < > " ')
}
},
},
],
},
})
const open = () => {
popup.value?.open()
}
const onConfirm = async () => {
try {
await formRef.value.validate()
if (form.new_password !== form.confrim_password) {
return uni.showToast({
title: '两次输入的新密码不一致',
icon: 'none',
mask: true,
})
}
await changePasswordApi({
old_password: form.old_password,
new_password: form.new_password,
})
uni.showToast({ title: '修改成功', icon: 'success' })
setTimeout(() => {
refreshPage()
}, 1500)
} catch (err) {
console.log(err)
}
}
defineExpose({ popup, open })
//
const resetPassword = () => {
resetPasswordRef.value.open()
}
</script>
<template>
<view>
<uni-popup ref="popup" type="dialog">
<uni-popup-dialog
ref="popupDialog"
mode="input"
title="修改密码"
@confirm="onConfirm"
@close="popupDialog.popup.close()"
:before-close="true"
>
<uni-forms
ref="formRef"
:model="form"
:rules="formRules"
validateTrigger="blur"
label-position="top"
>
<view class="remark">
<text>
密码未设置或忘记密码请选择
<text class="reset-btn" @tap="resetPassword">重置密码</text>
</text>
</view>
<uni-forms-item name="old_password" label="旧密码" required>
<uni-easyinput
v-model="form.old_password"
type="password"
placeholder="请输入旧密码"
maxlength="32"
/>
</uni-forms-item>
<uni-forms-item name="new_password" label="新密码" required>
<uni-easyinput
v-model="form.new_password"
type="password"
placeholder="请输入新密码"
maxlength="32"
/>
</uni-forms-item>
<uni-forms-item name="confrim_password" label="确认密码" required>
<uni-easyinput
v-model="form.confrim_password"
type="password"
placeholder="请再次输入新密码"
maxlength="32"
/>
</uni-forms-item>
</uni-forms>
<shop-reset-password ref="resetPasswordRef" />
</uni-popup-dialog>
</uni-popup>
</view>
</template>
<style lang="scss" scoped>
.remark {
font-size: 24rpx;
padding-bottom: 40rpx;
}
.reset-btn {
color: red;
font-size: 30rpx;
padding-left: 20rpx;
background: #fff;
&::after {
border: none;
}
}
</style>

View File

@ -1,24 +1,47 @@
<script setup lang="ts">
const props = defineProps<{
title?: string
content: string
}>()
const content = decodeURIComponent(props.content)
//
if (props.title) {
uni.setNavigationBarTitle({ title: props.title })
}
</script>
<template>
<view class="rich-text">
<mp-html :content="content" />
<mp-html :content="richText" />
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { editorApi } from '@/api/other.ts'
const props = defineProps<{
title ?: string
content : string
}>()
const content = decodeURIComponent(props.content)
const richText = ref()
const title = ref()
const params = {
key: 'expoApply'
}
onLoad(() => {
editor()
})
//
// if (props.title) {
// // uni.setNavigationBarTitle({ title: props.title })
// }
//
const editor = () => {
editorApi(params).then(res => {
if (res.code == 1) {
richText.value = res.data.editor
uni.setNavigationBarTitle({ title: res.data.title })
}
})
}
</script>
<style scoped lang="scss">
.rich-text {
padding: 20rpx 10rpx;
}
.rich-text {
padding: 30rpx;
text-indent: 60rpx;
}
</style>

View File

@ -7,8 +7,8 @@ import { onShowRefreshData } from '@/utils/common'
import { uploadApi } from '@/api/common'
import { reactive } from 'vue'
import { safeTop } from '@/utils/constants'
import changePassword from '@/pagesMember/pages/profile/components/change-password.vue'
import changePhone from '@/pagesMember/pages/profile/components/change-phone.vue'
import changePassword from './components/change-password.vue'
import changePhone from './components/change-phone.vue'
const memberStore = useMemberStore()

View File

@ -33,9 +33,7 @@ const onLogout = () => {
//
const toRichTextPage = (type: string) => {
const agreements = settngStore.data.agreement
const content = encodeURIComponent(agreements[type as keyof typeof agreements])
uni.navigateTo({
url: `${pageUrl['rich-text']}?content=${content}`,
})

View File

@ -209,8 +209,8 @@ const formatPrice = (price: string | number) => {
//
const goToIndex = (event: any) => {
event(false)
uni.switchTab({
url: `${pageUrl['index']}`,
uni.redirectTo({
url: `${pageUrl['shopping-mall']}`,
})
}
@ -280,7 +280,7 @@ watch(
@tap="onChangeSelected(item)"
></text>
<navigator
:url="`${pageUrl['goods-detail']}?id=${item.goods_id}`"
:url="`${pageUrl['shopping-mall-goods-detail']}?id=${item.goods_id}`"
hover-class="none"
class="navigator"
>

View File

@ -173,7 +173,7 @@ onShow(() => {
<navigator
class="goods"
hover-class="none"
:url="`${pageUrl['goods-detail']}?id=${item.id}`"
:url="`${pageUrl['shopping-mall-goods-detail']}?id=${item.id}`"
>
<view>
<image class="image" :src="fullUrl(item.images[0])" />

View File

@ -222,7 +222,10 @@ onShow(() => {
<!-- 商品信息 -->
<view class="goods" v-if="orderPreData">
<view v-for="item in orderPreData?.goods" :key="item.sku_id" class="item">
<navigator :url="`${pageUrl['goods-detail']}?id=${item.goods_id}`" hover-class="none">
<navigator
:url="`${pageUrl['shopping-mall-goods-detail']}?id=${item.goods_id}`"
hover-class="none"
>
<image class="picture" :src="fullUrl(item.image)" />
</navigator>
<view class="meta">

View File

@ -4,8 +4,8 @@ import { deleteOrderApi, getOrderDetailByIdApi, putOrderCancelByIdApi } from '@/
import type { orderResult } from '@/types/order'
import { ref, computed } from 'vue'
import { fullUrl, getPayPageUrl, formatPrice, onShowRefreshData } from '@/utils/common'
import receiveButton from '@/pagesOrder/components/receiveButton.vue'
import cancelButton from '@/pagesOrder/components/cancelButton.vue'
import receiveButton from '@/pagesShop/components/receiveButton.vue'
import cancelButton from '@/pagesShop/components/cancelButton.vue'
import { safeBottom } from '@/utils/constants'
//
@ -181,7 +181,7 @@ const onCancel = () => {
<view class="item" v-for="detail in detailData.detail" :key="detail.id">
<navigator
class="navigator"
:url="`${pageUrl['goods-detail']}?id=${detail.goods_id}`"
:url="`${pageUrl['shopping-mall-goods-detail']}?id=${detail.goods_id}`"
hover-class="none"
>
<view> <image class="cover" :src="fullUrl(detail.image)"></image> </view>

View File

@ -5,5 +5,5 @@ export const getShareUrl = (goodsId: number | string) => {
const settingData = useSettingStore().data
const profileData = useMemberStore().profile
return `${settingData.h5_domain}${pageUrl['goods-detail']}?id=${goodsId}&sharer=${profileData?.id}`
return `${settingData.h5_domain}${pageUrl['shopping-mall-goods-detail']}?id=${goodsId}&sharer=${profileData?.id}`
}

View File

@ -20,10 +20,10 @@ import { useMemberStore } from '@/stores'
import { getAddressApi } from '@/api/address'
import type { addressItem } from '@/types/address'
import { computeConversion } from '@/utils/common'
import servicePanel from '@/pagesGoods/pages/goods/components/servicePanel.vue'
import addressPanel from '@/pagesGoods/pages/goods/components//addressPanel.vue'
import sharePanel from '@/pagesGoods/pages/goods/components/sharePanel.vue'
import sharePoster from '@/pagesGoods/pages/goods/components/sharePoster.vue'
import servicePanel from './components/servicePanel.vue'
import addressPanel from './components/addressPanel.vue'
import sharePanel from './components/sharePanel.vue'
import sharePoster from './components/sharePoster.vue'
import { usePopupStore } from '@/stores'
const props = defineProps<{
@ -176,7 +176,7 @@ const openAddressPopup = () => {
const getMoreEvaluate = () => {
uni.navigateTo({
url: `${pageUrl['goods-evaluate']}?goods_id=${query.id}`,
url: `${pageUrl['shopping-mall-goods-evaluate']}?goods_id=${query.id}`,
})
}
@ -184,7 +184,7 @@ const cartInfo = ref(0)
const menuButtons = computed<UniHelper.UniGoodsNavOption[]>(() => [
{
icon: 'shop',
text: '首页',
text: '商城',
},
{
icon: 'cart',
@ -199,13 +199,13 @@ const menuButtons = computed<UniHelper.UniGoodsNavOption[]>(() => [
const menuClick = (event: UniHelper.UniGoodsNavOnClickEvent) => {
switch (event.index) {
case 0:
uni.switchTab({
url: `${pageUrl['index']}`,
uni.redirectTo({
url: `${pageUrl['shopping-mall']}`,
})
break
case 1:
uni.switchTab({
url: `${pageUrl['cart']}`,
uni.redirectTo({
url: `${pageUrl['shopping-mall-cart']}`,
})
break
case 2:
@ -303,7 +303,7 @@ const onShowPoster = () => {
onShareAppMessage(() => {
sharePopupRef.value.close()
const sharePath = `${pageUrl['goods-detail']}?id=${goods.value?.id}&sharer=${
const sharePath = `${pageUrl['shopping-mall-goods-detail']}?id=${goods.value?.id}&sharer=${
useMemberStore().profile?.id
}`
console.log(sharePath)

View File

@ -20,14 +20,14 @@ const storeHistroy = () => {
const search = async () => {
await uni.navigateTo({
url: `${pageUrl['goods-list']}?name=${name.value}`,
url: `${pageUrl['shopping-mall-goods-list']}?name=${name.value}`,
})
storeHistroy()
}
const onTapHistory = async (keyword: string) => {
await uni.navigateTo({
url: `${pageUrl['goods-list']}?name=${keyword}`,
url: `${pageUrl['shopping-mall-goods-list']}?name=${keyword}`,
})
name.value = keyword
storeHistroy()

View File

@ -12,9 +12,9 @@ import {
} from '@/utils/constants'
import { fullUrl, getPayPageUrl } from '@/utils/common'
import { onShow } from '@dcloudio/uni-app'
import cancelButton from '@/pagesOrder/components/cancelButton.vue'
import cancelButton from '@/pagesShop/components/cancelButton.vue'
import { usePopupStore } from '@/stores'
import ReceiveButton from '@/pagesOrder/components/receiveButton.vue'
import ReceiveButton from '@/pagesShop/components/receiveButton.vue'
// eslint-disable-next-line no-undef
const paging = ref<ZPagingInstance>()

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import orderListItem from '@/pagesOrder/pages/list/components/order-list-item.vue'
import orderListItem from './components/order-list-item.vue'
import { usePopupStore } from '@/stores'
import { pageMateStyle } from '@/utils/constants'

View File

@ -10,8 +10,8 @@ const props = defineProps<{
const change = (event: UniHelper.UniGridOnChangeEvent) => {
uni.setStorageSync('activeCategoryIndex', event.detail.index)
uni.switchTab({
url: pageUrl['category'],
uni.navigateTo({
url: pageUrl['shopping-mall-category'],
})
}
const groupedCategoryList = computed(() => {

View File

@ -16,12 +16,12 @@ const props = withDefaults(
const jumpTo = (item: hotRecommendItem) => {
if (item.goods_id) {
return uni.navigateTo({
url: `${pageUrl['goods-detail']}?id=${item.goods_id}`,
url: `${pageUrl['shopping-mall-goods-detail']}?id=${item.goods_id}`,
})
}
if (item.classify_id) {
return uni.navigateTo({
url: `${pageUrl['goods-list']}?classify_id=${item.classify_id}`,
url: `${pageUrl['shopping-mall-goods-list']}?classify_id=${item.classify_id}`,
})
}
}

View File

@ -0,0 +1,105 @@
<script setup lang="ts">
import { nextTick, ref } from 'vue'
import { getBannerApi, getHotRecommendApi, getNoticeBartApi } from '@/api/home'
import type { bannerItem, noticeBarItem } from '@/types/home'
import { onLoad, onShow } from '@dcloudio/uni-app'
import categoryGroup from './components/categoryGroup.vue'
import noticeBar from './components/notice-bar.vue'
import hotRecommend from './components/hotRecommend.vue'
import { getTopCategoryApi } from '@/api/catogory'
import { useCartStore, useMemberStore } from '@/stores'
import { getCartTotalNumApi } from '@/api/cart'
//
const bannerList = ref<bannerItem[]>([])
const getBannerListData = async () => {
const res = await getBannerApi()
bannerList.value = res.result
}
//
const noticeBarList = ref<noticeBarItem[]>([])
const getNoticeBarData = async () => {
const res = await getNoticeBartApi()
noticeBarList.value = res.result
}
//
const categoryList = ref()
const getTopCategoryListData = async () => {
const res = await getTopCategoryApi()
categoryList.value = res.result
}
//
const hotRecommendList = ref()
const getHotRecommend = async () => {
const res = await getHotRecommendApi()
hotRecommendList.value = res.result
}
//
const shopGoodsListRef = ref()
const getIndexData = () => {
return Promise.all([
getBannerListData(),
getNoticeBarData(),
getTopCategoryListData(),
getHotRecommend(),
])
}
onLoad(() => {
getIndexData()
//
// #ifdef MP-WEIXIN
uni.showShareMenu({
withShareTicket: true,
menus: ['shareAppMessage'],
})
// #endif
})
onShow(() => {
nextTick(() => {
shopGoodsListRef.value.refresh()
})
const cartStore = useCartStore()
//
if (useMemberStore().profile) {
getCartTotalNumApi().then((res) => {
cartStore.setCartTotalNum(res.result)
})
} else {
cartStore.setCartTotalNum(0)
}
cartStore.setCartTabBadge()
})
const handleRefresh = async (status : number) => {
//
if (status === 2) {
getIndexData()
}
}
</script>
<template>
<shop-goods-list ref="shopGoodsListRef" @refresher-status-changed="handleRefresh" :options="{
auto: false,
}">
<template #top>
<shop-goods-search />
</template>
<template #middle>
<shop-swiper :list="bannerList" v-if="bannerList" />
<noticeBar :list="noticeBarList" />
<category-group :list="categoryList" v-if="categoryList" />
<hot-recommend :list="hotRecommendList" />
<uni-section title="商品列表" type="line"></uni-section>
</template>
</shop-goods-list>
</template>
<style lang="scss">
page {
background-color: #f9f9f9;
}
</style>

View File

@ -60,8 +60,8 @@ onShow(() => {
<navigator
hover-class="none"
class="button navigator"
:url="`${pageUrl['index']}`"
open-type="switchTab"
:url="`${pageUrl['shopping-mall']}`"
open-type="redirect"
>
返回首页
</navigator>

View File

@ -135,7 +135,7 @@ onLoad(async () => {
<text class="title">售后商品</text>
<navigator
class="goods"
:url="`${pageUrl['goods-detail']}?id=${orderData.detail.goods_id}`"
:url="`${pageUrl['shopping-mall-goods-detail']}?id=${orderData.detail.goods_id}`"
hover-class="none"
>
<image class="cover" :src="fullUrl(orderData.detail.image)"></image>

View File

@ -97,7 +97,7 @@ const onSubmit = async () => {
<view class="item">
<navigator
class="navigator"
:url="`${pageUrl['goods-detail']}?id=${detailData.goods_id}`"
:url="`${pageUrl['shopping-mall-goods-detail']}?id=${detailData.goods_id}`"
hover-class="none"
>
<image class="cover" :src="fullUrl(detailData.image)"></image>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 852 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

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