账户状态\待办事项\客服\客户管理页面迁移

This commit is contained in:
陈善美 2025-08-28 11:37:15 +08:00
parent 0d17baa9c9
commit 65c88c24eb
17 changed files with 4185 additions and 45 deletions

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

958
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -59,9 +59,11 @@
"@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",
"wot-design-uni": "^1.12.3"

View File

@ -55,3 +55,11 @@ export const configIndex = (data : { page : number, page_size : number }) => {
data,
})
}
// 移动端-首页接口-搜索接口
export const configSearch = (data:Record<string, any>) => {
return request({
url: '/api/index/search',
method: 'POST',
data,
})
}

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

@ -49,6 +49,30 @@
"navigationStyle": "custom",
"navigationBarTitleText": "个人中心"
}
},
{
"path": "pages/black/index",
"style": {
"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": "客服"
}
}
],
"globalStyle": {
@ -80,12 +104,6 @@
"iconPath": "/static/tabs/cart_default.png",
"selectedIconPath": "/static/tabs/cart_selected.png"
},
// {
// "text": "我的",
// "pagePath": "pages/my/my",
// "iconPath": "/static/tabs/user_default.png",
// "selectedIconPath": "/static/tabs/user_selected.png"
// }
{
"text": "个人中心",
"pagePath": "pages/my/userCenter",
@ -361,7 +379,7 @@
}]
},
{
"root": "pagesEnterprise",
"root": "pageCompany",
"pages": [{
"path": "pages/portrait/index",
"style": {
@ -371,6 +389,16 @@
// #endif
}
}]
},
{
"root": "pagesCustomer",
"pages": [{
"path": "pages/customer/customer",
"style": {
"navigationBarTitleText": "客户管理",
"navigationStyle": "custom"
}
}]
}
],
"preloadRule": {

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>

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

@ -0,0 +1,188 @@
<template>
<view class="home-page" :style="backgroundStyle">
<!-- 顶部渐变背景 -->
<view :style="topGradientStyle"></view>
<!-- 状态栏占位 -->
<view class="status-bar"></view>
<!-- 小程序端右上角胶囊菜单栏占位 -->
<!-- #ifdef MP-WEIXIN -->
<view class="capsule-menu-section"></view>
<!-- #endif -->
<!-- 搜索栏 -->
<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"
@noticeClick="handleNoticeClick" />
</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 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')
const handleNoticeClick = (index : number) => {
console.log('handleNoticeClick', index)
// uni.navigateTo({
// url: '/pagesOther/pages/blank/rich-text',
// })
}
// tab
const handleTabChange = ({ index, tab } : { index : number; tab : any }) => {
activeTab.value = index
activeKey.value = tab.keys
// tab
}
//
const handleSearch = (keyword : string) => {
// tabContentRef.value.queryParams.keywords = keyword
// tabContentRef.value.getList()
//
}
//
const handleSearchInput = (keyword : string) => {
tabContentRef.value.queryParams.keywords = keyword
tabContentRef.value.getList()
// console.log(':', keyword)
}
//
const handleClearSearch = () => {
tabContentRef.value.queryParams.keywords = ''
tabContentRef.value.getList()
console.log('清空搜索')
}
//
onMounted(() => {
initPage()
})
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: 0 32rpx;
// 60rpx-100rpx
padding-bottom: 60rpx;
}
.status-bar {
height: 88rpx;
background: transparent;
position: relative;
z-index: 2;
}
.capsule-menu-section {
height: 100rpx;
}
.search-section {
position: relative;
z-index: 2;
margin-bottom: 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

@ -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>

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

@ -0,0 +1,498 @@
<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,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>