first
This commit is contained in:
623
src/pages/agent-system/asset-detail/index.vue
Normal file
623
src/pages/agent-system/asset-detail/index.vue
Normal file
@@ -0,0 +1,623 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { getAssetDetail, getAssetPackages, getCurrentPackage, getAssetHistory, stopCard, startCard, stopDevice, startDevice } from '@/api/assets'
|
||||
import type { AssetType, CardInfo, DeviceInfo, AssetPackage, HistoryRecord } from '@/api/assets'
|
||||
|
||||
// 资产类型
|
||||
const assetType = ref<AssetType>('card')
|
||||
|
||||
// 资产ID
|
||||
const assetId = ref('')
|
||||
|
||||
// 资产详情数据
|
||||
const assetDetail = ref<CardInfo | DeviceInfo | null>(null)
|
||||
|
||||
// 实时状态数据
|
||||
const realtimeStatus = ref<any>(null)
|
||||
|
||||
// 套餐列表
|
||||
const packageList = ref<AssetPackage[]>([])
|
||||
|
||||
// 历史记录列表
|
||||
const historyList = ref<HistoryRecord[]>([])
|
||||
|
||||
// 当前套餐
|
||||
const currentPackage = ref<AssetPackage | null>(null)
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
const refreshing = ref(false)
|
||||
const operating = ref(false)
|
||||
|
||||
// 当前Tab
|
||||
const activeTab = ref<'info' | 'package' | 'history'>('info')
|
||||
|
||||
// Tab选项
|
||||
const tabs = [
|
||||
{ key: 'info', label: '基本信息', icon: 'i-mdi-information' },
|
||||
{ key: 'package', label: '套餐信息', icon: 'i-mdi-package-variant' },
|
||||
{ key: 'history', label: '操作历史', icon: 'i-mdi-history' },
|
||||
]
|
||||
|
||||
// 状态文本
|
||||
const statusText = computed(() => {
|
||||
if (!realtimeStatus.value)
|
||||
return '未知'
|
||||
return realtimeStatus.value.status === 'active' ? '正常' : '停机'
|
||||
})
|
||||
|
||||
// 状态颜色
|
||||
const statusColor = computed(() => {
|
||||
if (!realtimeStatus.value)
|
||||
return { bg: '#f5f5f5', text: '#999' }
|
||||
return realtimeStatus.value.status === 'active'
|
||||
? { bg: '#e8f5e9', text: '#2e7d32' }
|
||||
: { bg: '#ffebee', text: '#c62828' }
|
||||
})
|
||||
|
||||
// 页面加载
|
||||
onMounted(() => {
|
||||
// 从路由参数获取 assetType 和 assetId
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1] as any
|
||||
assetType.value = currentPage.options.type as AssetType
|
||||
assetId.value = currentPage.options.id
|
||||
|
||||
loadAssetDetail()
|
||||
})
|
||||
|
||||
// 加载资产详情
|
||||
async function loadAssetDetail() {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 调用资产详情接口
|
||||
const detailData = await getAssetDetail(assetType.value, assetId.value)
|
||||
|
||||
// 根据类型提取详情
|
||||
if (assetType.value === 'card' && detailData.card) {
|
||||
assetDetail.value = detailData.card
|
||||
realtimeStatus.value = {
|
||||
status: detailData.card.status,
|
||||
data_usage: detailData.card.data_usage,
|
||||
data_total: detailData.card.data_total,
|
||||
last_update: new Date().toLocaleString('zh-CN'),
|
||||
}
|
||||
}
|
||||
else if (assetType.value === 'device' && detailData.device) {
|
||||
assetDetail.value = detailData.device
|
||||
realtimeStatus.value = {
|
||||
status: detailData.device.status,
|
||||
card_count: detailData.device.card_count,
|
||||
last_online_time: detailData.device.last_online_time,
|
||||
last_update: new Date().toLocaleString('zh-CN'),
|
||||
}
|
||||
}
|
||||
|
||||
// 加载套餐列表
|
||||
await loadPackages()
|
||||
|
||||
// 加载历史记录
|
||||
if (activeTab.value === 'history') {
|
||||
await loadHistory()
|
||||
}
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('加载资产详情失败:', error)
|
||||
// 如果是前端逻辑错误(非 API 错误),需要手动显示提示
|
||||
if (error instanceof Error && error.message && !error.statusCode) {
|
||||
uni.$u.toast(error.message)
|
||||
}
|
||||
// API 请求错误已由拦截器统一处理
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载套餐列表
|
||||
async function loadPackages() {
|
||||
try {
|
||||
// 1. 调用当前生效套餐接口
|
||||
try {
|
||||
const current = await getCurrentPackage(assetType.value, Number(assetId.value))
|
||||
currentPackage.value = current
|
||||
} catch (error) {
|
||||
console.warn('获取当前套餐失败:', error)
|
||||
currentPackage.value = null
|
||||
}
|
||||
|
||||
// 2. 调用套餐列表接口
|
||||
const packagesData = await getAssetPackages(assetType.value, Number(assetId.value), {
|
||||
page: 1,
|
||||
page_size: 50
|
||||
})
|
||||
packageList.value = packagesData.items || []
|
||||
|
||||
// 如果没有获取到当前套餐,从列表中查找生效中的套餐
|
||||
if (!currentPackage.value && packageList.value.length > 0) {
|
||||
currentPackage.value = packageList.value.find(pkg => pkg.status === 1) || null
|
||||
}
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('加载套餐列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载历史记录
|
||||
async function loadHistory() {
|
||||
try {
|
||||
const historyData = await getAssetHistory(assetType.value, assetId.value, {
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
})
|
||||
historyList.value = historyData.list
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('加载历史记录失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新状态
|
||||
async function refreshStatus() {
|
||||
refreshing.value = true
|
||||
|
||||
try {
|
||||
// 重新加载数据
|
||||
await loadAssetDetail()
|
||||
|
||||
uni.showToast({
|
||||
title: '刷新成功',
|
||||
icon: 'success',
|
||||
})
|
||||
}
|
||||
catch (error: any) {
|
||||
// 如果是前端逻辑错误(非 API 错误),需要手动显示提示
|
||||
if (error instanceof Error && error.message && !error.statusCode) {
|
||||
uni.$u.toast(error.message)
|
||||
}
|
||||
// API 请求错误已由拦截器统一处理
|
||||
}
|
||||
finally {
|
||||
refreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 切换Tab时加载对应数据
|
||||
async function handleTabChange(tab: 'info' | 'package' | 'history') {
|
||||
activeTab.value = tab
|
||||
|
||||
if (tab === 'history' && historyList.value.length === 0) {
|
||||
await loadHistory()
|
||||
}
|
||||
}
|
||||
|
||||
// 停机
|
||||
async function stopAsset() {
|
||||
uni.showModal({
|
||||
title: '确认停机',
|
||||
content: '停机后将无法使用网络服务,确定要停机吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
operating.value = true
|
||||
try {
|
||||
// 调用停机接口
|
||||
if (assetType.value === 'card') {
|
||||
await stopCard({ id: assetId.value })
|
||||
}
|
||||
else {
|
||||
await stopDevice({ id: assetId.value })
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: '停机成功',
|
||||
icon: 'success',
|
||||
})
|
||||
|
||||
await loadAssetDetail()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('停机失败:', error)
|
||||
uni.showToast({
|
||||
title: error.msg || '停机失败',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
finally {
|
||||
operating.value = false
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 复机
|
||||
async function startAsset() {
|
||||
uni.showModal({
|
||||
title: '确认复机',
|
||||
content: '复机后将恢复网络服务,确定要复机吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
operating.value = true
|
||||
try {
|
||||
// 调用复机接口
|
||||
if (assetType.value === 'card') {
|
||||
await startCard({ id: assetId.value })
|
||||
}
|
||||
else {
|
||||
await startDevice({ id: assetId.value })
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: '复机成功',
|
||||
icon: 'success',
|
||||
})
|
||||
|
||||
await loadAssetDetail()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('复机失败:', error)
|
||||
uni.showToast({
|
||||
title: error.msg || '复机失败',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
finally {
|
||||
operating.value = false
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 查看钱包 (仅代理端)
|
||||
function viewWallet() {
|
||||
// 跳转到钱包页面
|
||||
uni.navigateTo({
|
||||
url: `/pages/agent-system/asset-wallet/index?type=${assetType.value}&id=${assetId.value}`,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="bg-[#fafafa] min-h-screen pb-safe">
|
||||
<!-- 加载中 -->
|
||||
<view v-if="loading" class="pt-20 center">
|
||||
<i class="i-mdi-loading animate-spin text-40px text-[#999]" />
|
||||
</view>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<view v-else-if="assetDetail" class="px-4 pt-4">
|
||||
<!-- 状态卡片 -->
|
||||
<view class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden mb-3">
|
||||
<!-- 顶部信息 -->
|
||||
<view class="px-4 pt-4 pb-3 border-b-1px border-[#f5f5f5]">
|
||||
<view class="flex items-center justify-between mb-3">
|
||||
<view class="flex items-center gap-2">
|
||||
<i
|
||||
:class="[
|
||||
'text-28px',
|
||||
assetType === 'card' ? 'i-mdi-sim' : 'i-mdi-devices',
|
||||
'text-[#212121]',
|
||||
]"
|
||||
/>
|
||||
<view>
|
||||
<text class="text-18px font-700 text-[#212121] block">
|
||||
{{ assetType === 'card' ? '物联网卡' : '物联网设备' }}
|
||||
</text>
|
||||
<text class="text-12px text-[#999]">
|
||||
ID: {{ assetDetail.id || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
:style="{
|
||||
backgroundColor: statusColor.bg,
|
||||
color: statusColor.text,
|
||||
}"
|
||||
class="px-3 py-1 rounded-8px text-13px font-600"
|
||||
>
|
||||
{{ statusText }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text class="text-13px text-[#999]">
|
||||
{{ assetDetail.operator || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 实时状态 -->
|
||||
<view class="px-4 py-4">
|
||||
<text class="text-14px font-600 text-[#212121] block mb-3">
|
||||
实时状态
|
||||
</text>
|
||||
|
||||
<view class="grid grid-cols-2 gap-3">
|
||||
<!-- 流量使用 -->
|
||||
<view class="bg-[#f5f5f5] rounded-12px p-3">
|
||||
<text class="text-12px text-[#999] block mb-1">
|
||||
流量使用
|
||||
</text>
|
||||
<text class="text-16px font-700 text-[#212121] block">
|
||||
{{ realtimeStatus?.data_usage || '-' }}
|
||||
</text>
|
||||
<text class="text-11px text-[#999]">
|
||||
/ {{ realtimeStatus?.data_total || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 语音使用 -->
|
||||
<view class="bg-[#f5f5f5] rounded-12px p-3">
|
||||
<text class="text-12px text-[#999] block mb-1">
|
||||
语音使用
|
||||
</text>
|
||||
<text class="text-16px font-700 text-[#212121] block">
|
||||
{{ realtimeStatus?.voice_usage || '-' }}
|
||||
</text>
|
||||
<text class="text-11px text-[#999]">
|
||||
/ {{ realtimeStatus?.voice_total || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text class="text-11px text-[#999] block mt-3">
|
||||
最后更新: {{ realtimeStatus?.last_update || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Tab切换 -->
|
||||
<view class="flex items-center gap-2 mb-3">
|
||||
<view
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
:class="[
|
||||
'flex-1 py-3 rounded-12px center flex-col gap-1',
|
||||
'transition-all duration-200',
|
||||
activeTab === tab.key
|
||||
? 'bg-[#212121] shadow-[0_2px_12px_rgba(0,0,0,0.1)]'
|
||||
: 'bg-white shadow-[0_2px_12px_rgba(0,0,0,0.06)]',
|
||||
]"
|
||||
@click="handleTabChange(tab.key)"
|
||||
>
|
||||
<i
|
||||
:class="[
|
||||
tab.icon,
|
||||
'text-20px',
|
||||
activeTab === tab.key ? 'text-white' : 'text-[#666]',
|
||||
]"
|
||||
/>
|
||||
<text
|
||||
:class="[
|
||||
'text-12px font-600',
|
||||
activeTab === tab.key ? 'text-white' : 'text-[#666]',
|
||||
]"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<view v-if="activeTab === 'info'" class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4 space-y-3">
|
||||
<view class="flex items-start justify-between">
|
||||
<text class="text-13px text-[#999] w-90px">ICCID</text>
|
||||
<text class="flex-1 text-14px text-[#212121] text-right break-all">
|
||||
{{ assetDetail.iccid || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="flex items-start justify-between">
|
||||
<text class="text-13px text-[#999] w-90px">虚拟号</text>
|
||||
<text class="flex-1 text-14px text-[#212121] text-right">
|
||||
{{ assetDetail.virtual_number || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="flex items-start justify-between">
|
||||
<text class="text-13px text-[#999] w-90px">IMEI</text>
|
||||
<text class="flex-1 text-14px text-[#212121] text-right break-all">
|
||||
{{ assetDetail.imei || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="flex items-start justify-between">
|
||||
<text class="text-13px text-[#999] w-90px">运营商</text>
|
||||
<text class="flex-1 text-14px text-[#212121] text-right">
|
||||
{{ assetDetail.operator || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="flex items-start justify-between">
|
||||
<text class="text-13px text-[#999] w-90px">创建时间</text>
|
||||
<text class="flex-1 text-14px text-[#212121] text-right">
|
||||
{{ assetDetail.create_time || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="flex items-start justify-between">
|
||||
<text class="text-13px text-[#999] w-90px">激活时间</text>
|
||||
<text class="flex-1 text-14px text-[#212121] text-right">
|
||||
{{ assetDetail.activate_time || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 套餐信息 -->
|
||||
<view v-if="activeTab === 'package'" class="space-y-3">
|
||||
<!-- 当前套餐 -->
|
||||
<view v-if="currentPackage" class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4">
|
||||
<view class="flex items-center justify-between mb-3">
|
||||
<text class="text-14px font-600 text-[#212121]">
|
||||
当前套餐
|
||||
</text>
|
||||
<view class="px-3 py-1 rounded-8px bg-[#e8f5e9] text-12px font-600 text-[#2e7d32]">
|
||||
{{ currentPackage.status_name || '-' }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text class="text-16px font-700 text-[#212121] block mb-2">
|
||||
{{ currentPackage.package_name || '-' }}
|
||||
</text>
|
||||
<text class="text-14px text-[#ff6700] font-600 block mb-3">
|
||||
已用: {{ currentPackage.virtual_used_mb || 0 }}MB / 总量: {{ currentPackage.virtual_limit_mb || 0 }}MB
|
||||
</text>
|
||||
|
||||
<view class="flex items-center justify-between text-12px text-[#999]">
|
||||
<text v-if="currentPackage.activated_at && currentPackage.expires_at">
|
||||
{{ currentPackage.activated_at }} ~ {{ currentPackage.expires_at }}
|
||||
</text>
|
||||
<text v-else>-</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 所有套餐列表 -->
|
||||
<view v-if="packageList.length > 0" class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4">
|
||||
<text class="text-14px font-600 text-[#212121] block mb-3">
|
||||
套餐列表
|
||||
</text>
|
||||
|
||||
<view
|
||||
v-for="pkg in packageList"
|
||||
:key="pkg.package_usage_id"
|
||||
class="py-3 border-t-1px border-[#f5f5f5] first:border-t-0 first:pt-0"
|
||||
>
|
||||
<view class="flex items-center justify-between mb-2">
|
||||
<text class="text-15px font-600 text-[#212121]">
|
||||
{{ pkg.package_name || '-' }}
|
||||
</text>
|
||||
<view
|
||||
class="px-2 py-0.5 rounded text-11px"
|
||||
:style="{
|
||||
backgroundColor: pkg.status === 1 ? '#e8f5e9' : '#f5f5f5',
|
||||
color: pkg.status === 1 ? '#2e7d32' : '#999'
|
||||
}"
|
||||
>
|
||||
{{ pkg.status_name || '-' }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex items-center justify-between mb-1">
|
||||
<text class="text-12px text-[#666]">
|
||||
已用: {{ pkg.virtual_used_mb || 0 }}MB / {{ pkg.virtual_limit_mb || 0 }}MB
|
||||
</text>
|
||||
<text class="text-11px text-[#999]">
|
||||
优先级: {{ pkg.priority || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
<text class="text-11px text-[#999]">
|
||||
{{ pkg.activated_at || '-' }} ~ {{ pkg.expires_at || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 暂无套餐 -->
|
||||
<view v-else class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4 py-12 center flex-col">
|
||||
<i class="i-mdi-package-variant-closed text-60px text-[#ddd] mb-2" />
|
||||
<text class="text-13px text-[#999]">
|
||||
暂无套餐数据
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作历史 -->
|
||||
<view v-if="activeTab === 'history'" class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4">
|
||||
<text class="text-14px font-600 text-[#212121] block mb-3">
|
||||
操作记录
|
||||
</text>
|
||||
|
||||
<!-- 暂无数据 -->
|
||||
<view class="py-8 center flex-col">
|
||||
<i class="i-mdi-history text-60px text-[#ddd] mb-2" />
|
||||
<text class="text-13px text-[#999]">
|
||||
暂无操作记录
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮区 -->
|
||||
<view class="mt-4 space-y-3">
|
||||
<!-- 停复机按钮 -->
|
||||
<view class="flex gap-3">
|
||||
<view
|
||||
v-if="realtimeStatus.status === 'active'"
|
||||
class="
|
||||
flex-1 py-3 rounded-12px center
|
||||
bg-[#ffebee]
|
||||
transition-all duration-200
|
||||
active:opacity-80
|
||||
"
|
||||
@click="stopAsset"
|
||||
>
|
||||
<i class="i-mdi-pause-circle text-18px text-[#c62828] mr-1" />
|
||||
<text class="text-15px font-600 text-[#c62828]">
|
||||
停机
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-else
|
||||
class="
|
||||
flex-1 py-3 rounded-12px center
|
||||
bg-[#e8f5e9]
|
||||
transition-all duration-200
|
||||
active:opacity-80
|
||||
"
|
||||
@click="startAsset"
|
||||
>
|
||||
<i class="i-mdi-play-circle text-18px text-[#2e7d32] mr-1" />
|
||||
<text class="text-15px font-600 text-[#2e7d32]">
|
||||
复机
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="
|
||||
flex-1 py-3 rounded-12px center
|
||||
bg-[#212121]
|
||||
transition-all duration-200
|
||||
active:bg-[#000]
|
||||
"
|
||||
@click="viewWallet"
|
||||
>
|
||||
<i class="i-mdi-wallet text-18px text-white mr-1" />
|
||||
<text class="text-15px font-600 text-white">
|
||||
查看钱包
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部留白 -->
|
||||
<view class="h-8" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.space-y-3 > view:not(:last-child) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.break-all {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
</style>
|
||||
1160
src/pages/agent-system/asset-search/index.vue
Normal file
1160
src/pages/agent-system/asset-search/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
252
src/pages/agent-system/asset-wallet/index.vue
Normal file
252
src/pages/agent-system/asset-wallet/index.vue
Normal file
@@ -0,0 +1,252 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getAssetWallet } from '@/api/wallet'
|
||||
import type { AssetWalletInfo, AssetType } from '@/api/wallet'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
|
||||
// 资产类型和ID (从URL参数获取)
|
||||
const assetType = ref<AssetType>('card')
|
||||
const assetId = ref<number>(0)
|
||||
|
||||
// 钱包信息
|
||||
const walletInfo = ref<AssetWalletInfo | null>(null)
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 格式化金额(分转元)
|
||||
function formatAmount(amount: number) {
|
||||
return `¥${(amount / 100).toFixed(2)}`
|
||||
}
|
||||
|
||||
// 获取钱包状态文本
|
||||
function getStatusText(status: number, statusText?: string) {
|
||||
if (statusText) return statusText
|
||||
const map: Record<number, string> = {
|
||||
1: '正常',
|
||||
2: '冻结',
|
||||
3: '已停用',
|
||||
}
|
||||
return map[status] || '未知'
|
||||
}
|
||||
|
||||
// 获取钱包状态颜色
|
||||
function getStatusColor(status: number) {
|
||||
const map: Record<number, { bg: string, text: string }> = {
|
||||
1: { bg: '#e8f5e9', text: '#2e7d32' }, // 正常
|
||||
2: { bg: '#fff3e0', text: '#e65100' }, // 冻结
|
||||
3: { bg: '#f5f5f5', text: '#999' }, // 已停用
|
||||
}
|
||||
return map[status] || { bg: '#f5f5f5', text: '#999' }
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
function formatTime(time: string) {
|
||||
if (!time) return '-'
|
||||
const date = new Date(time)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hour = String(date.getHours()).padStart(2, '0')
|
||||
const minute = String(date.getMinutes()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hour}:${minute}`
|
||||
}
|
||||
|
||||
// 页面加载时获取参数
|
||||
onLoad((options) => {
|
||||
if (options.asset_type) {
|
||||
assetType.value = options.asset_type as AssetType
|
||||
}
|
||||
if (options.asset_id) {
|
||||
assetId.value = Number(options.asset_id)
|
||||
}
|
||||
})
|
||||
|
||||
// 页面挂载
|
||||
onMounted(() => {
|
||||
if (assetId.value) {
|
||||
loadWalletInfo()
|
||||
}
|
||||
})
|
||||
|
||||
// 加载钱包信息
|
||||
async function loadWalletInfo() {
|
||||
if (!assetId.value) {
|
||||
uni.$u.toast('缺少资产ID')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const wallet = await getAssetWallet(assetType.value, assetId.value)
|
||||
walletInfo.value = wallet
|
||||
}
|
||||
catch (error) {
|
||||
console.error('加载资产钱包信息失败:', error)
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="bg-[#fafafa] min-h-screen pb-safe">
|
||||
<!-- 加载中 -->
|
||||
<view v-if="loading" class="pt-20 center">
|
||||
<i class="i-mdi-loading animate-spin text-40px text-[#999]" />
|
||||
</view>
|
||||
|
||||
<!-- 钱包信息 -->
|
||||
<view v-else-if="walletInfo" class="px-4 pt-4">
|
||||
<!-- 余额卡片 -->
|
||||
<view class="bg-gradient-to-r from-[#ff6700] to-[#ff8534] rounded-16px shadow-[0_4px_16px_rgba(255,103,0,0.3)] p-4 mb-3">
|
||||
<view class="flex items-center justify-between mb-3">
|
||||
<view>
|
||||
<text class="text-13px text-white/70 block mb-1">
|
||||
可用余额
|
||||
</text>
|
||||
<text class="text-32px font-700 text-white block">
|
||||
{{ formatAmount(walletInfo.available_balance) }}
|
||||
</text>
|
||||
</view>
|
||||
<view
|
||||
:style="{
|
||||
backgroundColor: getStatusColor(walletInfo.status).bg,
|
||||
color: getStatusColor(walletInfo.status).text,
|
||||
}"
|
||||
class="px-3 py-1 rounded-8px text-12px font-600"
|
||||
>
|
||||
{{ getStatusText(walletInfo.status, walletInfo.status_text) }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="grid grid-cols-2 gap-3 pt-3 border-t-1px border-white/20">
|
||||
<view>
|
||||
<text class="text-11px text-white/70 block mb-1">总余额</text>
|
||||
<text class="text-14px font-600 text-white">
|
||||
{{ formatAmount(walletInfo.balance) }}
|
||||
</text>
|
||||
</view>
|
||||
<view>
|
||||
<text class="text-11px text-white/70 block mb-1">冻结金额</text>
|
||||
<text class="text-14px font-600 text-white">
|
||||
{{ formatAmount(walletInfo.frozen_balance) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 钱包详情 -->
|
||||
<view class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4 mb-3">
|
||||
<view class="flex items-center gap-2 mb-3">
|
||||
<i class="i-mdi-information-outline text-20px text-[#ff6700]" />
|
||||
<text class="text-15px font-600 text-[#212121]">
|
||||
钱包详情
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="space-y-2">
|
||||
<view class="flex items-center justify-between">
|
||||
<text class="text-13px text-[#666]">钱包ID</text>
|
||||
<text class="text-13px text-[#212121]">
|
||||
{{ walletInfo.wallet_id }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="flex items-center justify-between">
|
||||
<text class="text-13px text-[#666]">资产ID</text>
|
||||
<text class="text-13px text-[#212121]">
|
||||
{{ walletInfo.resource_id }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="flex items-center justify-between">
|
||||
<text class="text-13px text-[#666]">资产类型</text>
|
||||
<text class="text-13px text-[#212121]">
|
||||
{{ walletInfo.resource_type === 'iot_card' ? 'IoT卡' : '设备' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="flex items-center justify-between">
|
||||
<text class="text-13px text-[#666]">币种</text>
|
||||
<text class="text-13px text-[#212121]">
|
||||
{{ walletInfo.currency }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="flex items-center justify-between">
|
||||
<text class="text-13px text-[#666]">创建时间</text>
|
||||
<text class="text-11px text-[#999]">
|
||||
{{ formatTime(walletInfo.created_at) }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="flex items-center justify-between">
|
||||
<text class="text-13px text-[#666]">更新时间</text>
|
||||
<text class="text-11px text-[#999]">
|
||||
{{ formatTime(walletInfo.updated_at) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 温馨提示 -->
|
||||
<view class="bg-[#fff3e0] rounded-12px p-3 mb-3">
|
||||
<view class="flex items-start gap-2">
|
||||
<i class="i-mdi-information text-16px text-[#e65100] mt-0.5" />
|
||||
<view class="flex-1">
|
||||
<text class="text-12px text-[#666] block">
|
||||
此钱包为资产专属钱包,余额仅可用于该资产相关的费用支付。
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-else class="pt-20 center flex-col">
|
||||
<i class="i-mdi-wallet-outline text-60px text-[#ddd] mb-3" />
|
||||
<text class="text-14px text-[#999] mb-2">
|
||||
暂无钱包信息
|
||||
</text>
|
||||
<text class="text-12px text-[#ccc]">
|
||||
请检查资产ID是否正确
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 底部留白 -->
|
||||
<view class="h-8" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.space-y-2 > view:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
</style>
|
||||
841
src/pages/agent-system/assets/index.vue
Normal file
841
src/pages/agent-system/assets/index.vue
Normal file
@@ -0,0 +1,841 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { getUserInfo } from '@/api/auth'
|
||||
import type { UserInfo } from '@/api/auth'
|
||||
import { getEnterpriseCards, getEnterpriseDevices } from '@/api/enterprise'
|
||||
import { getCarriers } from '@/api/carrier'
|
||||
import type { CarrierInfo } from '@/api/carrier'
|
||||
import { stopDeviceAsset, startDeviceAsset, stopCardByIccid, startCardByIccid } from '@/api/assets'
|
||||
import type { DeviceStopResponse } from '@/api/assets'
|
||||
import Skeleton from '@/components/Skeleton.vue'
|
||||
import SimplePicker from '@/components/SimplePicker.vue'
|
||||
import { formatTime, formatDataSize } from '@/utils/format'
|
||||
import { get } from '@/utils/request'
|
||||
|
||||
// 卡片信息接口
|
||||
interface CardInfo {
|
||||
id: number
|
||||
iccid: string
|
||||
imsi?: string
|
||||
msisdn?: string
|
||||
virtual_no?: string
|
||||
virtual_number?: string // 企业端字段
|
||||
status?: number
|
||||
status_name?: string
|
||||
activation_status?: number
|
||||
network_status?: number
|
||||
network_status_name?: string
|
||||
real_name_status?: number
|
||||
carrier_name?: string
|
||||
carrier_type?: string
|
||||
operator?: string // 企业端字段
|
||||
supplier?: string
|
||||
batch_no?: string
|
||||
series_name?: string
|
||||
shop_name?: string
|
||||
data_usage_mb?: number
|
||||
current_month_usage_mb?: number
|
||||
accumulated_recharge?: number
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
// 设备信息接口
|
||||
interface DeviceInfo {
|
||||
id?: number
|
||||
device_id?: number // 企业端字段
|
||||
device_name?: string
|
||||
device_model?: string
|
||||
device_type?: string
|
||||
manufacturer?: string
|
||||
imei?: string
|
||||
sn?: string
|
||||
virtual_no?: string
|
||||
status?: number
|
||||
status_name?: string
|
||||
online_status?: number
|
||||
bound_card_count?: number
|
||||
card_count?: number // 企业端字段
|
||||
max_sim_slots?: number
|
||||
switch_mode?: string
|
||||
series_name?: string
|
||||
shop_name?: string
|
||||
software_version?: string
|
||||
accumulated_recharge?: number
|
||||
activated_at?: string
|
||||
authorized_at?: string // 企业端字段
|
||||
last_online_time?: string
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
// 用户信息
|
||||
const userInfo = ref<UserInfo | null>(null)
|
||||
|
||||
// 获取单卡列表(代理端)
|
||||
function getStandaloneCards(params: any) {
|
||||
return get<{ items: CardInfo[], page: number, size: number, total: number }>('/api/admin/iot-cards/standalone', { params })
|
||||
}
|
||||
|
||||
// 获取设备列表(代理端)
|
||||
function getDevices(params: any) {
|
||||
return get<{ items: DeviceInfo[], page: number, size: number, total: number }>('/api/admin/devices', { params })
|
||||
}
|
||||
|
||||
// 企业ID (企业端使用)
|
||||
const enterpriseId = ref<number>(0)
|
||||
|
||||
// 资产类型
|
||||
type AssetType = 'card' | 'device'
|
||||
|
||||
// 统一的资产项
|
||||
interface AssetItem {
|
||||
id: string
|
||||
type: AssetType
|
||||
name: string
|
||||
status: string
|
||||
statusColor: string
|
||||
detail: string
|
||||
raw: CardInfo | DeviceInfo
|
||||
}
|
||||
|
||||
// 当前标签
|
||||
const activeTab = ref<'card' | 'device'>('card')
|
||||
// 资产列表
|
||||
const cardList = ref<CardInfo[]>([])
|
||||
const deviceList = ref<DeviceInfo[]>([])
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
// 分页
|
||||
const cardPage = ref(1)
|
||||
const devicePage = ref(1)
|
||||
const cardHasMore = ref(true)
|
||||
const deviceHasMore = ref(true)
|
||||
const pageSize = 20
|
||||
// 搜索和筛选
|
||||
const iccidKeyword = ref('')
|
||||
const virtualNoKeyword = ref('')
|
||||
const carrierFilter = ref<number | undefined>(undefined)
|
||||
const statusFilter = ref<number | undefined>(undefined)
|
||||
const showCarrierPicker = ref(false)
|
||||
const showStatusPicker = ref(false)
|
||||
// 运营商列表
|
||||
const carrierList = ref<CarrierInfo[]>([])
|
||||
// 运营商选择器选项
|
||||
const carrierOptions = computed(() => [
|
||||
{ label: '全部运营商', value: undefined },
|
||||
...carrierList.value.map(c => ({ label: c.carrier_name, value: c.id }))
|
||||
])
|
||||
// 状态选择器选项
|
||||
const statusOptions = [
|
||||
{ label: '全部状态', value: undefined },
|
||||
{ label: '在库', value: 1 },
|
||||
{ label: '已分销', value: 2 },
|
||||
{ label: '已激活', value: 3 },
|
||||
{ label: '已停用', value: 4 }
|
||||
]
|
||||
|
||||
// 合并后的资产列表
|
||||
const assetList = computed<AssetItem[]>(() => {
|
||||
const cards: AssetItem[] = cardList.value.map(card => ({
|
||||
id: String(card.id),
|
||||
type: 'card' as AssetType,
|
||||
name: card.iccid,
|
||||
// 兼容企业端和代理端的状态字段
|
||||
status: card.network_status_name || (card.network_status === 1 ? '开机' : card.network_status === 0 ? '停机' : card.status_name || '未知'),
|
||||
statusColor: (card.network_status === 1) ? '#52c41a' : (card.network_status === 0) ? '#999' : '#ff4d4f',
|
||||
// 兼容企业端和代理端的字段
|
||||
detail: `虚拟号: ${card.virtual_no || card.virtual_number || '-'} | 运营商: ${card.carrier_name || card.operator || '-'}`,
|
||||
raw: card,
|
||||
}))
|
||||
|
||||
const devices: AssetItem[] = deviceList.value.map(device => ({
|
||||
id: String(device.id || device.device_id),
|
||||
type: 'device' as AssetType,
|
||||
name: device.device_name || '-',
|
||||
// 兼容企业端和代理端的状态字段
|
||||
status: device.status_name || (device.authorized_at ? '已授权' : device.online_status === 1 ? '在线' : device.online_status === 2 ? '离线' : '未知'),
|
||||
statusColor: (device.authorized_at || device.online_status === 1) ? '#52c41a' : (device.online_status === 2) ? '#999' : '#ff4d4f',
|
||||
// 兼容企业端和代理端的字段
|
||||
detail: `虚拟号: ${device.virtual_no || '-'} | 型号: ${device.device_model || '-'} | 卡数: ${device.bound_card_count || device.card_count || 0}`,
|
||||
raw: device,
|
||||
}))
|
||||
|
||||
if (activeTab.value === 'card') return cards
|
||||
if (activeTab.value === 'device') return devices
|
||||
return cards
|
||||
})
|
||||
|
||||
|
||||
// 获取企业ID(企业端使用)
|
||||
async function getEnterpriseId() {
|
||||
if (enterpriseId.value) return enterpriseId.value
|
||||
|
||||
const cachedUserInfo = uni.getStorageSync('user_info')
|
||||
if (cachedUserInfo?.enterprise_id) {
|
||||
enterpriseId.value = cachedUserInfo.enterprise_id
|
||||
return enterpriseId.value
|
||||
}
|
||||
|
||||
const apiUserInfo = await getUserInfo()
|
||||
if (!apiUserInfo.enterprise_id) {
|
||||
throw new Error('未找到企业ID')
|
||||
}
|
||||
enterpriseId.value = apiUserInfo.enterprise_id
|
||||
return enterpriseId.value
|
||||
}
|
||||
|
||||
// 加载IoT卡列表(兼容企业端和代理端)
|
||||
async function loadCards(isRefresh = false) {
|
||||
if (loading.value || (!cardHasMore.value && !isRefresh)) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
// 如果是刷新,重置页码
|
||||
if (isRefresh) {
|
||||
cardPage.value = 1
|
||||
cardList.value = []
|
||||
cardHasMore.value = true
|
||||
}
|
||||
|
||||
const params: any = {
|
||||
page: cardPage.value,
|
||||
page_size: pageSize,
|
||||
}
|
||||
|
||||
// 添加筛选条件
|
||||
if (carrierFilter.value !== undefined) {
|
||||
params.carrier_id = carrierFilter.value
|
||||
}
|
||||
if (statusFilter.value !== undefined) {
|
||||
params.status = statusFilter.value
|
||||
}
|
||||
if (iccidKeyword.value) {
|
||||
params.iccid = iccidKeyword.value
|
||||
}
|
||||
if (virtualNoKeyword.value) {
|
||||
params.virtual_no = virtualNoKeyword.value
|
||||
}
|
||||
|
||||
let cardsData: any
|
||||
|
||||
// 根据用户类型调用不同接口
|
||||
if (userInfo.value?.user_type === 3) {
|
||||
// 代理端:调用代理接口
|
||||
cardsData = await getStandaloneCards(params)
|
||||
} else {
|
||||
// 企业端:调用企业接口
|
||||
const id = await getEnterpriseId()
|
||||
cardsData = await getEnterpriseCards(id, params)
|
||||
}
|
||||
|
||||
const newItems = cardsData?.items || []
|
||||
|
||||
if (cardPage.value === 1) {
|
||||
cardList.value = newItems
|
||||
} else {
|
||||
cardList.value.push(...newItems)
|
||||
}
|
||||
|
||||
// 判断是否还有更多数据
|
||||
cardHasMore.value = newItems.length >= pageSize
|
||||
cardPage.value++
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('加载IoT卡列表失败:', error)
|
||||
if (error instanceof Error && error.message && !error.statusCode) {
|
||||
uni.$u.toast(error.message)
|
||||
}
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载设备列表(兼容企业端和代理端)
|
||||
async function loadDevices(isRefresh = false) {
|
||||
if (loading.value || (!deviceHasMore.value && !isRefresh)) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
// 如果是刷新,重置页码
|
||||
if (isRefresh) {
|
||||
devicePage.value = 1
|
||||
deviceList.value = []
|
||||
deviceHasMore.value = true
|
||||
}
|
||||
|
||||
const params: any = {
|
||||
page: devicePage.value,
|
||||
page_size: pageSize,
|
||||
}
|
||||
|
||||
if (virtualNoKeyword.value) {
|
||||
params.virtual_no = virtualNoKeyword.value
|
||||
}
|
||||
if (statusFilter.value !== undefined) {
|
||||
params.status = statusFilter.value
|
||||
}
|
||||
|
||||
let devicesData: any
|
||||
|
||||
// 根据用户类型调用不同接口
|
||||
if (userInfo.value?.user_type === 3) {
|
||||
// 代理端:调用代理接口
|
||||
devicesData = await getDevices(params)
|
||||
} else {
|
||||
// 企业端:调用企业接口
|
||||
const id = await getEnterpriseId()
|
||||
devicesData = await getEnterpriseDevices(id, params)
|
||||
}
|
||||
|
||||
const newItems = devicesData?.items || []
|
||||
|
||||
if (devicePage.value === 1) {
|
||||
deviceList.value = newItems
|
||||
} else {
|
||||
deviceList.value.push(...newItems)
|
||||
}
|
||||
|
||||
// 判断是否还有更多数据
|
||||
deviceHasMore.value = newItems.length >= pageSize
|
||||
devicePage.value++
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('加载设备列表失败:', error)
|
||||
if (error instanceof Error && error.message && !error.statusCode) {
|
||||
uni.$u.toast(error.message)
|
||||
}
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
function loadMore() {
|
||||
if (activeTab.value === 'card') {
|
||||
loadCards()
|
||||
} else {
|
||||
loadDevices()
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新列表
|
||||
function refreshList() {
|
||||
if (activeTab.value === 'card') {
|
||||
loadCards(true)
|
||||
} else {
|
||||
loadDevices(true)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载运营商列表
|
||||
async function loadCarriers() {
|
||||
try {
|
||||
const response = await getCarriers({ page: 1, page_size: 100 })
|
||||
carrierList.value = response?.items || []
|
||||
} catch (error) {
|
||||
console.error('加载运营商列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 选择运营商
|
||||
function onCarrierConfirm(option: { label: string, value: any }) {
|
||||
carrierFilter.value = option.value
|
||||
refreshList()
|
||||
}
|
||||
|
||||
// 选择状态
|
||||
function onStatusConfirm(option: { label: string, value: any }) {
|
||||
statusFilter.value = option.value
|
||||
refreshList()
|
||||
}
|
||||
|
||||
// 获取当前选中的运营商名称
|
||||
const selectedCarrierName = computed(() => {
|
||||
if (carrierFilter.value === undefined) return '运营商'
|
||||
const option = carrierOptions.value.find(o => o.value === carrierFilter.value)
|
||||
return option?.label || '运营商'
|
||||
})
|
||||
|
||||
// 获取当前选中的状态名称
|
||||
const selectedStatusName = computed(() => {
|
||||
if (statusFilter.value === undefined) return '状态'
|
||||
const option = statusOptions.find(o => o.value === statusFilter.value)
|
||||
return option?.label || '状态'
|
||||
})
|
||||
|
||||
// 查看资产详情
|
||||
function viewAssetDetail(asset: AssetItem) {
|
||||
if (asset.type === 'card') {
|
||||
const card = asset.raw as CardInfo
|
||||
uni.navigateTo({
|
||||
url: `/pages/agent-system/asset-search/index?iccid=${card.iccid}`,
|
||||
})
|
||||
} else {
|
||||
const device = asset.raw as DeviceInfo
|
||||
uni.navigateTo({
|
||||
url: `/pages/agent-system/asset-search/index?virtual_no=${device.virtual_no}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 切换标签
|
||||
function changeTab(tab: 'card' | 'device') {
|
||||
if (activeTab.value === tab) return
|
||||
|
||||
activeTab.value = tab
|
||||
iccidKeyword.value = ''
|
||||
virtualNoKeyword.value = ''
|
||||
carrierFilter.value = undefined
|
||||
statusFilter.value = undefined
|
||||
|
||||
// 切换后加载对应数据
|
||||
if (tab === 'card' && cardList.value.length === 0) {
|
||||
loadCards(true)
|
||||
} else if (tab === 'device' && deviceList.value.length === 0) {
|
||||
loadDevices(true)
|
||||
}
|
||||
}
|
||||
|
||||
// 设备停机
|
||||
async function handleStopDevice(asset: AssetItem, event: Event) {
|
||||
event.stopPropagation()
|
||||
|
||||
const device = asset.raw as DeviceInfo
|
||||
const deviceId = device.id || device.device_id
|
||||
|
||||
if (!deviceId) {
|
||||
uni.$u.toast('设备ID不存在')
|
||||
return
|
||||
}
|
||||
|
||||
uni.showModal({
|
||||
title: '确认停机',
|
||||
content: `确定要停机设备"${device.device_name}"吗?这将停机设备下所有已实名卡。`,
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showLoading({ title: '停机中...' })
|
||||
try {
|
||||
const result = await stopDeviceAsset(Number(deviceId))
|
||||
uni.hideLoading()
|
||||
|
||||
// 显示停机结果
|
||||
const msg = `停机完成\n成功: ${result.success_count}张\n失败: ${result.fail_count}张\n跳过: ${result.skip_count}张`
|
||||
uni.showModal({
|
||||
title: '停机结果',
|
||||
content: msg,
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
// 刷新列表
|
||||
refreshList()
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
uni.hideLoading()
|
||||
// 错误信息已经在拦截器中通过 toast 显示了,这里不需要重复显示
|
||||
console.error('设备停机失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 设备复机
|
||||
async function handleStartDevice(asset: AssetItem, event: Event) {
|
||||
event.stopPropagation()
|
||||
|
||||
const device = asset.raw as DeviceInfo
|
||||
const deviceId = device.id || device.device_id
|
||||
|
||||
if (!deviceId) {
|
||||
uni.$u.toast('设备ID不存在')
|
||||
return
|
||||
}
|
||||
|
||||
uni.showModal({
|
||||
title: '确认复机',
|
||||
content: `确定要复机设备"${device.device_name}"吗?这将复机设备下所有已实名卡。`,
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showLoading({ title: '复机中...' })
|
||||
try {
|
||||
await startDeviceAsset(Number(deviceId))
|
||||
uni.hideLoading()
|
||||
uni.$u.toast('复机成功')
|
||||
// 刷新列表
|
||||
refreshList()
|
||||
} catch (error: any) {
|
||||
uni.hideLoading()
|
||||
// 错误信息已经在拦截器中通过 toast 显示了,这里不需要重复显示
|
||||
console.error('设备复机失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 单卡停机
|
||||
async function handleStopCard(asset: AssetItem, event: Event) {
|
||||
event.stopPropagation()
|
||||
|
||||
const card = asset.raw as CardInfo
|
||||
|
||||
if (!card.iccid) {
|
||||
uni.$u.toast('ICCID不存在')
|
||||
return
|
||||
}
|
||||
|
||||
uni.showModal({
|
||||
title: '确认停机',
|
||||
content: `确定要停机卡"${card.iccid}"吗?`,
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showLoading({ title: '停机中...' })
|
||||
try {
|
||||
await stopCardByIccid(card.iccid)
|
||||
uni.hideLoading()
|
||||
uni.$u.toast('停机成功')
|
||||
// 刷新列表
|
||||
refreshList()
|
||||
} catch (error: any) {
|
||||
uni.hideLoading()
|
||||
// 错误信息已经在拦截器中通过 toast 显示了,这里不需要重复显示
|
||||
console.error('单卡停机失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 单卡复机
|
||||
async function handleStartCard(asset: AssetItem, event: Event) {
|
||||
event.stopPropagation()
|
||||
|
||||
const card = asset.raw as CardInfo
|
||||
|
||||
if (!card.iccid) {
|
||||
uni.$u.toast('ICCID不存在')
|
||||
return
|
||||
}
|
||||
|
||||
uni.showModal({
|
||||
title: '确认复机',
|
||||
content: `确定要复机卡"${card.iccid}"吗?`,
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showLoading({ title: '复机中...' })
|
||||
try {
|
||||
await startCardByIccid(card.iccid)
|
||||
uni.hideLoading()
|
||||
uni.$u.toast('复机成功')
|
||||
// 刷新列表
|
||||
refreshList()
|
||||
} catch (error: any) {
|
||||
uni.hideLoading()
|
||||
// 错误信息已经在拦截器中通过 toast 显示了,这里不需要重复显示
|
||||
console.error('单卡复机失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 滚动事件处理
|
||||
function onScroll(e: any) {
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.target
|
||||
// 当滚动到底部附近时加载更多
|
||||
if (scrollHeight - scrollTop - clientHeight < 100) {
|
||||
loadMore()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 1. 加载用户信息(判断是企业端还是代理端)
|
||||
try {
|
||||
const user = await getUserInfo()
|
||||
userInfo.value = user
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
}
|
||||
|
||||
// 2. 加载运营商列表
|
||||
loadCarriers()
|
||||
|
||||
// 3. 默认加载IoT卡列表
|
||||
loadCards()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="bg-page safe-area-bottom min-h-screen">
|
||||
<!-- 搜索和筛选 -->
|
||||
<view class="safe-area-top px-4 pt-4 pb-3">
|
||||
<!-- IoT卡搜索和筛选 -->
|
||||
<view v-if="activeTab === 'card'">
|
||||
<!-- ICCID搜索 -->
|
||||
<view class="bg-white rounded-full px-4 py-2 flex items-center mb-2">
|
||||
<i class="i-mdi-credit-card text-lg text-[#94a3b8] mr-2" />
|
||||
<input
|
||||
v-model="iccidKeyword"
|
||||
class="flex-1 text-sm text-[#1e293b]"
|
||||
placeholder="ICCID"
|
||||
/>
|
||||
<i
|
||||
v-if="iccidKeyword"
|
||||
class="i-mdi-close-circle text-lg text-[#94a3b8] ml-2"
|
||||
@click="iccidKeyword = ''"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 虚拟号搜索 -->
|
||||
<view class="bg-white rounded-full px-4 py-2 flex items-center mb-2">
|
||||
<i class="i-mdi-phone text-lg text-[#94a3b8] mr-2" />
|
||||
<input
|
||||
v-model="virtualNoKeyword"
|
||||
class="flex-1 text-sm text-[#1e293b]"
|
||||
placeholder="虚拟号"
|
||||
/>
|
||||
<i
|
||||
v-if="virtualNoKeyword"
|
||||
class="i-mdi-close-circle text-lg text-[#94a3b8] ml-2"
|
||||
@click="virtualNoKeyword = ''"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 筛选条件 -->
|
||||
<view class="flex items-center gap-2 mb-2">
|
||||
<!-- 运营商 -->
|
||||
<view class="flex-1 bg-white rounded-full px-3 py-2 flex items-center" @click="showCarrierPicker = true">
|
||||
<text class="flex-1 text-sm text-[#1e293b]">{{ selectedCarrierName }}</text>
|
||||
<i class="i-mdi-chevron-down text-lg text-[#94a3b8]" />
|
||||
</view>
|
||||
|
||||
<!-- 状态 -->
|
||||
<view class="flex-1 bg-white rounded-full px-3 py-2 flex items-center" @click="showStatusPicker = true">
|
||||
<text class="flex-1 text-sm text-[#1e293b]">{{ selectedStatusName }}</text>
|
||||
<i class="i-mdi-chevron-down text-lg text-[#94a3b8]" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索按钮 -->
|
||||
<view class="bg-brand rounded-full py-2.5 text-center" @click="refreshList">
|
||||
<text class="text-sm font-500 text-white">搜索</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 设备搜索 -->
|
||||
<view v-else>
|
||||
<view class="bg-white rounded-full px-4 py-2 flex items-center mb-2">
|
||||
<i class="i-mdi-phone text-lg text-[#94a3b8] mr-2" />
|
||||
<input
|
||||
v-model="virtualNoKeyword"
|
||||
class="flex-1 text-sm text-[#1e293b]"
|
||||
placeholder="虚拟号"
|
||||
/>
|
||||
<i
|
||||
v-if="virtualNoKeyword"
|
||||
class="i-mdi-close-circle text-lg text-[#94a3b8] ml-2"
|
||||
@click="virtualNoKeyword = ''"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 搜索按钮 -->
|
||||
<view class="bg-brand rounded-full py-2.5 text-center" @click="refreshList">
|
||||
<text class="text-sm font-500 text-white">搜索</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 筛选标签 -->
|
||||
<view class="px-4 pb-3">
|
||||
<view class="bg-white rounded-full p-1 flex items-center">
|
||||
<!-- IoT卡 -->
|
||||
<view
|
||||
class="flex-1 py-2 rounded-full transition-fast text-center"
|
||||
:class="activeTab === 'card' ? 'bg-brand' : ''"
|
||||
@click="changeTab('card')"
|
||||
>
|
||||
<text class="text-sm font-500" :class="activeTab === 'card' ? 'text-white' : 'text-[#64748b]'">IoT卡</text>
|
||||
</view>
|
||||
|
||||
<!-- 设备 -->
|
||||
<view
|
||||
class="flex-1 py-2 rounded-full transition-fast text-center"
|
||||
:class="activeTab === 'device' ? 'bg-brand' : ''"
|
||||
@click="changeTab('device')"
|
||||
>
|
||||
<text class="text-sm font-500" :class="activeTab === 'device' ? 'text-white' : 'text-[#64748b]'">设备</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<Skeleton v-if="loading && assetList.length === 0" type="list" />
|
||||
|
||||
<!-- 资产列表 -->
|
||||
<view
|
||||
v-else
|
||||
class="overflow-y-auto"
|
||||
:style="{ height: activeTab === 'card' ? 'calc(100vh - 380px)' : 'calc(100vh - 280px)' }"
|
||||
@scroll="onScroll"
|
||||
>
|
||||
<view v-if="assetList.length > 0" class="px-4 pb-4">
|
||||
<view
|
||||
v-for="asset in assetList"
|
||||
:key="`${asset.type}-${asset.id}`"
|
||||
class="card shadow-sm mb-3 cursor-pointer transition-fast hover:shadow-md active:scale-98"
|
||||
@click="viewAssetDetail(asset)"
|
||||
>
|
||||
<!-- 头部区域 -->
|
||||
<view class="flex items-start justify-between mb-3">
|
||||
<!-- 左侧信息 -->
|
||||
<view class="flex items-start gap-2 flex-1 min-w-0">
|
||||
<!-- 状态指示点 -->
|
||||
<view
|
||||
class="w-2 h-2 rounded-full mt-1 flex-shrink-0"
|
||||
:style="{ backgroundColor: asset.statusColor }"
|
||||
/>
|
||||
|
||||
<!-- 资产名称 -->
|
||||
<view class="flex-1 min-w-0">
|
||||
<text class="text-base font-600 text-primary block truncate">
|
||||
{{ asset.name }}
|
||||
</text>
|
||||
<text class="text-xs text-tertiary mt-1 block">
|
||||
{{ asset.detail }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 类型徽章 -->
|
||||
<view
|
||||
class="px-2 py-1 radius-sm text-xs font-500 ml-2 flex-shrink-0"
|
||||
:style="{
|
||||
backgroundColor: asset.type === 'card' ? 'rgba(255, 103, 0, 0.1)' : 'rgba(51, 51, 51, 0.1)',
|
||||
color: asset.type === 'card' ? 'var(--brand-primary)' : 'var(--brand-secondary)',
|
||||
}"
|
||||
>
|
||||
{{ asset.type === 'card' ? 'IoT卡' : '设备' }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部状态栏 -->
|
||||
<view class="flex items-center justify-between pt-3 border-t border-border-secondary">
|
||||
<!-- 状态标签 -->
|
||||
<view class="flex items-center gap-1">
|
||||
<view
|
||||
class="w-1.5 h-1.5 rounded-full"
|
||||
:style="{ backgroundColor: asset.statusColor }"
|
||||
/>
|
||||
<text class="text-sm font-500" :style="{ color: asset.statusColor }">
|
||||
{{ asset.status }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮组 -->
|
||||
<view class="flex items-center gap-2">
|
||||
<!-- IoT卡操作按钮 -->
|
||||
<template v-if="asset.type === 'card'">
|
||||
<!-- 根据网络状态显示对应按钮 -->
|
||||
<template v-if="(asset.raw as CardInfo).network_status === 0">
|
||||
<!-- 已停机,只显示复机按钮 -->
|
||||
<view
|
||||
class="px-3 py-1 rounded-lg flex items-center justify-center"
|
||||
style="background: #52c41a; color: white;"
|
||||
@click="handleStartCard(asset, $event)"
|
||||
>
|
||||
<text class="text-xs font-500">复机</text>
|
||||
</view>
|
||||
</template>
|
||||
<template v-else-if="(asset.raw as CardInfo).network_status === 1">
|
||||
<!-- 正常运行,只显示停机按钮 -->
|
||||
<view
|
||||
class="px-3 py-1 rounded-lg flex items-center justify-center"
|
||||
style="background: #ff4d4f; color: white;"
|
||||
@click="handleStopCard(asset, $event)"
|
||||
>
|
||||
<text class="text-xs font-500">停机</text>
|
||||
</view>
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- 其他状态,显示两个按钮 -->
|
||||
<view
|
||||
class="px-3 py-1 rounded-lg flex items-center justify-center"
|
||||
style="background: #ff4d4f; color: white;"
|
||||
@click="handleStopCard(asset, $event)"
|
||||
>
|
||||
<text class="text-xs font-500">停机</text>
|
||||
</view>
|
||||
<view
|
||||
class="px-3 py-1 rounded-lg flex items-center justify-center"
|
||||
style="background: #52c41a; color: white;"
|
||||
@click="handleStartCard(asset, $event)"
|
||||
>
|
||||
<text class="text-xs font-500">复机</text>
|
||||
</view>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- 设备操作按钮 - 始终显示两个按钮 -->
|
||||
<template v-else>
|
||||
<view
|
||||
class="px-3 py-1 rounded-lg flex items-center justify-center"
|
||||
style="background: #ff4d4f; color: white;"
|
||||
@click="handleStopDevice(asset, $event)"
|
||||
>
|
||||
<text class="text-xs font-500">停机</text>
|
||||
</view>
|
||||
<view
|
||||
class="px-3 py-1 rounded-lg flex items-center justify-center"
|
||||
style="background: #52c41a; color: white;"
|
||||
@click="handleStartDevice(asset, $event)"
|
||||
>
|
||||
<text class="text-xs font-500">复机</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 箭头 -->
|
||||
<i class="i-mdi-chevron-right text-xl text-quaternary" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多提示 -->
|
||||
<view v-if="loading" class="py-4 text-center">
|
||||
<i class="i-mdi-loading animate-spin text-xl text-brand mr-2" />
|
||||
<text class="text-sm text-[#64748b]">加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="!((activeTab === 'card' && cardHasMore) || (activeTab === 'device' && deviceHasMore))" class="py-4 text-center">
|
||||
<text class="text-sm text-[#94a3b8]">没有更多了</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-else class="px-4">
|
||||
<view class="bg-white rounded-2xl py-12 flex flex-col items-center">
|
||||
<i class="i-mdi-package-variant-closed text-6xl text-[#cbd5e1] mb-3" />
|
||||
<text class="text-base text-[#64748b] mb-1">暂无数据</text>
|
||||
<text class="text-sm text-[#94a3b8]">{{ iccidKeyword || virtualNoKeyword ? '未找到相关结果' : '暂无资产数据' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 运营商选择器 -->
|
||||
<SimplePicker
|
||||
v-model:show="showCarrierPicker"
|
||||
:options="carrierOptions"
|
||||
title="选择运营商"
|
||||
@confirm="onCarrierConfirm"
|
||||
/>
|
||||
|
||||
<!-- 状态选择器 -->
|
||||
<SimplePicker
|
||||
v-model:show="showStatusPicker"
|
||||
:options="statusOptions"
|
||||
title="选择状态"
|
||||
@confirm="onStatusConfirm"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
438
src/pages/agent-system/change-password/index.vue
Normal file
438
src/pages/agent-system/change-password/index.vue
Normal file
@@ -0,0 +1,438 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { changePassword } from '@/api/auth'
|
||||
|
||||
// 表单数据
|
||||
const formData = ref({
|
||||
old_password: '',
|
||||
new_password: '',
|
||||
confirm_password: '',
|
||||
})
|
||||
|
||||
// 密码可见性
|
||||
const passwordVisible = ref({
|
||||
old: false,
|
||||
new: false,
|
||||
confirm: false,
|
||||
})
|
||||
|
||||
// 提交状态
|
||||
const submitting = ref(false)
|
||||
|
||||
// 密码强度
|
||||
const passwordStrength = computed(() => {
|
||||
const password = formData.value.new_password
|
||||
if (!password)
|
||||
return { level: 0, text: '', color: '' }
|
||||
|
||||
let strength = 0
|
||||
// 长度检查
|
||||
if (password.length >= 8)
|
||||
strength++
|
||||
if (password.length >= 12)
|
||||
strength++
|
||||
// 复杂度检查
|
||||
if (/[a-z]/.test(password))
|
||||
strength++
|
||||
if (/[A-Z]/.test(password))
|
||||
strength++
|
||||
if (/\d/.test(password))
|
||||
strength++
|
||||
if (/[^a-zA-Z\d]/.test(password))
|
||||
strength++
|
||||
|
||||
if (strength <= 2)
|
||||
return { level: 1, text: '弱', color: '#ff6b6b' }
|
||||
if (strength <= 4)
|
||||
return { level: 2, text: '中', color: '#e68815' }
|
||||
return { level: 3, text: '强', color: '#3ed268' }
|
||||
})
|
||||
|
||||
// 表单验证
|
||||
const isFormValid = computed(() => {
|
||||
const { old_password, new_password, confirm_password } = formData.value
|
||||
return (
|
||||
old_password.length > 0
|
||||
&& new_password.length >= 6
|
||||
&& new_password === confirm_password
|
||||
)
|
||||
})
|
||||
|
||||
// 切换密码可见性
|
||||
function togglePasswordVisible(field: 'old' | 'new' | 'confirm') {
|
||||
passwordVisible.value[field] = !passwordVisible.value[field]
|
||||
}
|
||||
|
||||
// 提交修改
|
||||
async function submitChange() {
|
||||
// 验证表单
|
||||
if (!formData.value.old_password) {
|
||||
uni.showToast({
|
||||
title: '请输入当前密码',
|
||||
icon: 'none',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (formData.value.new_password.length < 6) {
|
||||
uni.showToast({
|
||||
title: '新密码至少6位',
|
||||
icon: 'none',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (formData.value.new_password !== formData.value.confirm_password) {
|
||||
uni.showToast({
|
||||
title: '两次密码输入不一致',
|
||||
icon: 'none',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (formData.value.old_password === formData.value.new_password) {
|
||||
uni.showToast({
|
||||
title: '新密码不能与当前密码相同',
|
||||
icon: 'none',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
|
||||
try {
|
||||
// 调用修改密码接口
|
||||
await changePassword({
|
||||
old_password: formData.value.old_password,
|
||||
new_password: formData.value.new_password,
|
||||
})
|
||||
|
||||
uni.showToast({
|
||||
title: '密码修改成功',
|
||||
icon: 'success',
|
||||
duration: 2000,
|
||||
})
|
||||
|
||||
// 清除登录状态
|
||||
uni.removeStorageSync('admin-token')
|
||||
uni.removeStorageSync('refresh_token')
|
||||
uni.removeStorageSync('user_info')
|
||||
|
||||
// 延迟跳转到登录页
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/common/login/index',
|
||||
})
|
||||
}, 2000)
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('修改密码失败:', error)
|
||||
// 如果是前端逻辑错误(非 API 错误),需要手动显示提示
|
||||
if (error instanceof Error && error.message && !error.statusCode) {
|
||||
uni.$u.toast(error.message)
|
||||
}
|
||||
// API 请求错误已由拦截器统一处理
|
||||
}
|
||||
finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 清空表单
|
||||
function clearForm() {
|
||||
formData.value = {
|
||||
old_password: '',
|
||||
new_password: '',
|
||||
confirm_password: '',
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="bg-[#fafafa] min-h-screen pb-safe">
|
||||
<!-- 内容区域 -->
|
||||
<view class="px-4 pt-5">
|
||||
<!-- 提示信息 -->
|
||||
<view class="bg-[#e3f2fd] rounded-12px p-4 mb-4 flex items-start gap-3">
|
||||
<i class="i-mdi-information text-20px text-[#1976d2] mt-0.5" />
|
||||
<view class="flex-1">
|
||||
<text class="text-13px text-[#1976d2] block mb-1 font-600">
|
||||
密码安全提示
|
||||
</text>
|
||||
<text class="text-12px text-[#1976d2] leading-relaxed">
|
||||
为了您的账号安全,建议密码长度至少8位,包含大小写字母、数字和特殊字符。
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 表单卡片 -->
|
||||
<view class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4 mb-4">
|
||||
<!-- 当前密码 -->
|
||||
<view class="mb-4">
|
||||
<text class="text-13px text-[#999] block mb-2">
|
||||
当前密码 <text class="text-[#ff6b6b]">*</text>
|
||||
</text>
|
||||
<view class="flex items-center gap-2 px-4 py-3 rounded-12px bg-[#f5f5f5]">
|
||||
<i class="i-mdi-lock text-20px text-[#999]" />
|
||||
<input
|
||||
v-model="formData.old_password"
|
||||
:password="!passwordVisible.old"
|
||||
class="flex-1 text-15px text-[#212121] placeholder:text-[#ccc]"
|
||||
placeholder="请输入当前密码"
|
||||
>
|
||||
<i
|
||||
:class="[
|
||||
'text-20px text-[#999]',
|
||||
passwordVisible.old ? 'i-mdi-eye-off' : 'i-mdi-eye',
|
||||
]"
|
||||
@click="togglePasswordVisible('old')"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 新密码 -->
|
||||
<view class="mb-4">
|
||||
<view class="flex items-center justify-between mb-2">
|
||||
<text class="text-13px text-[#999]">
|
||||
新密码 <text class="text-[#ff6b6b]">*</text>
|
||||
</text>
|
||||
<view
|
||||
v-if="formData.new_password"
|
||||
class="flex items-center gap-1"
|
||||
>
|
||||
<text class="text-11px text-[#999]">
|
||||
强度:
|
||||
</text>
|
||||
<text
|
||||
:style="{ color: passwordStrength.color }"
|
||||
class="text-11px font-600"
|
||||
>
|
||||
{{ passwordStrength.text }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex items-center gap-2 px-4 py-3 rounded-12px bg-[#f5f5f5]">
|
||||
<i class="i-mdi-lock-outline text-20px text-[#999]" />
|
||||
<input
|
||||
v-model="formData.new_password"
|
||||
:password="!passwordVisible.new"
|
||||
class="flex-1 text-15px text-[#212121] placeholder:text-[#ccc]"
|
||||
placeholder="请输入新密码(至少6位)"
|
||||
>
|
||||
<i
|
||||
:class="[
|
||||
'text-20px text-[#999]',
|
||||
passwordVisible.new ? 'i-mdi-eye-off' : 'i-mdi-eye',
|
||||
]"
|
||||
@click="togglePasswordVisible('new')"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 密码强度指示器 -->
|
||||
<view
|
||||
v-if="formData.new_password"
|
||||
class="flex items-center gap-1 mt-2"
|
||||
>
|
||||
<view
|
||||
v-for="i in 3"
|
||||
:key="i"
|
||||
:class="[
|
||||
'flex-1 h-2px rounded-full',
|
||||
i <= passwordStrength.level ? 'opacity-100' : 'opacity-20',
|
||||
]"
|
||||
:style="{ backgroundColor: passwordStrength.color }"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 确认密码 -->
|
||||
<view>
|
||||
<text class="text-13px text-[#999] block mb-2">
|
||||
确认密码 <text class="text-[#ff6b6b]">*</text>
|
||||
</text>
|
||||
<view class="flex items-center gap-2 px-4 py-3 rounded-12px bg-[#f5f5f5]">
|
||||
<i class="i-mdi-lock-check text-20px text-[#999]" />
|
||||
<input
|
||||
v-model="formData.confirm_password"
|
||||
:password="!passwordVisible.confirm"
|
||||
class="flex-1 text-15px text-[#212121] placeholder:text-[#ccc]"
|
||||
placeholder="请再次输入新密码"
|
||||
>
|
||||
<i
|
||||
:class="[
|
||||
'text-20px text-[#999]',
|
||||
passwordVisible.confirm ? 'i-mdi-eye-off' : 'i-mdi-eye',
|
||||
]"
|
||||
@click="togglePasswordVisible('confirm')"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 密码匹配提示 -->
|
||||
<view
|
||||
v-if="formData.confirm_password"
|
||||
class="flex items-center gap-1 mt-2"
|
||||
>
|
||||
<i
|
||||
:class="[
|
||||
'text-14px',
|
||||
formData.new_password === formData.confirm_password
|
||||
? 'i-mdi-check-circle text-[#3ed268]'
|
||||
: 'i-mdi-close-circle text-[#ff6b6b]',
|
||||
]"
|
||||
/>
|
||||
<text
|
||||
:class="[
|
||||
'text-12px',
|
||||
formData.new_password === formData.confirm_password
|
||||
? 'text-[#3ed268]'
|
||||
: 'text-[#ff6b6b]',
|
||||
]"
|
||||
>
|
||||
{{ formData.new_password === formData.confirm_password ? '密码一致' : '密码不一致' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 密码要求说明 -->
|
||||
<view class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4 mb-4">
|
||||
<text class="text-14px font-600 text-[#212121] block mb-3">
|
||||
密码要求
|
||||
</text>
|
||||
|
||||
<view class="space-y-2">
|
||||
<view class="flex items-start gap-2">
|
||||
<i
|
||||
:class="[
|
||||
'text-14px mt-0.5',
|
||||
formData.new_password.length >= 6
|
||||
? 'i-mdi-check-circle text-[#3ed268]'
|
||||
: 'i-mdi-checkbox-blank-circle-outline text-[#ccc]',
|
||||
]"
|
||||
/>
|
||||
<text class="flex-1 text-13px text-[#666]">
|
||||
至少6个字符(建议8个以上)
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="flex items-start gap-2">
|
||||
<i
|
||||
:class="[
|
||||
'text-14px mt-0.5',
|
||||
/[a-zA-Z]/.test(formData.new_password)
|
||||
? 'i-mdi-check-circle text-[#3ed268]'
|
||||
: 'i-mdi-checkbox-blank-circle-outline text-[#ccc]',
|
||||
]"
|
||||
/>
|
||||
<text class="flex-1 text-13px text-[#666]">
|
||||
包含字母
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="flex items-start gap-2">
|
||||
<i
|
||||
:class="[
|
||||
'text-14px mt-0.5',
|
||||
/\d/.test(formData.new_password)
|
||||
? 'i-mdi-check-circle text-[#3ed268]'
|
||||
: 'i-mdi-checkbox-blank-circle-outline text-[#ccc]',
|
||||
]"
|
||||
/>
|
||||
<text class="flex-1 text-13px text-[#666]">
|
||||
包含数字
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="flex items-start gap-2">
|
||||
<i
|
||||
:class="[
|
||||
'text-14px mt-0.5',
|
||||
/[^a-zA-Z\d]/.test(formData.new_password)
|
||||
? 'i-mdi-check-circle text-[#3ed268]'
|
||||
: 'i-mdi-checkbox-blank-circle-outline text-[#ccc]',
|
||||
]"
|
||||
/>
|
||||
<text class="flex-1 text-13px text-[#666]">
|
||||
包含特殊字符(推荐)
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="space-y-3">
|
||||
<!-- 确认修改 -->
|
||||
<view
|
||||
:class="[
|
||||
'py-3 rounded-12px center',
|
||||
'transition-all duration-200',
|
||||
isFormValid && !submitting
|
||||
? 'bg-[#212121] active:bg-[#000]'
|
||||
: 'bg-[#ccc]',
|
||||
]"
|
||||
@click="submitChange"
|
||||
>
|
||||
<i
|
||||
v-if="submitting"
|
||||
class="i-mdi-loading animate-spin text-18px text-white mr-2"
|
||||
/>
|
||||
<text class="text-15px font-600 text-white">
|
||||
{{ submitting ? '修改中...' : '确认修改' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 清空表单 -->
|
||||
<view
|
||||
class="
|
||||
py-3 rounded-12px center
|
||||
bg-white shadow-[0_2px_12px_rgba(0,0,0,0.06)]
|
||||
transition-all duration-200
|
||||
active:bg-[#f5f5f5]
|
||||
"
|
||||
@click="clearForm"
|
||||
>
|
||||
<i class="i-mdi-refresh text-18px text-[#666] mr-2" />
|
||||
<text class="text-15px font-600 text-[#666]">
|
||||
清空
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部留白 -->
|
||||
<view class="h-8" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.space-y-3 > view:not(:last-child) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.space-y-2 > view:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.leading-relaxed {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
</style>
|
||||
460
src/pages/agent-system/commission-center/index.vue
Normal file
460
src/pages/agent-system/commission-center/index.vue
Normal file
@@ -0,0 +1,460 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { getCommissionSummary, getCommissionRecords, getCommissionStats } from '@/api/commission'
|
||||
import type { CommissionSummary, CommissionRecord, CommissionStats } from '@/api/commission'
|
||||
import SimplePicker from '@/components/SimplePicker.vue'
|
||||
|
||||
// 佣金概览
|
||||
const commissionSummary = ref<CommissionSummary | null>(null)
|
||||
|
||||
// 佣金明细列表
|
||||
const commissionRecords = ref<CommissionRecord[]>([])
|
||||
|
||||
// 佣金统计
|
||||
const commissionStats = ref<CommissionStats | null>(null)
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
const loadingRecords = ref(false)
|
||||
|
||||
// 分页参数
|
||||
const page = ref(1)
|
||||
const pageSize = 20
|
||||
const total = ref(0)
|
||||
|
||||
// 搜索关键词
|
||||
const searchKeyword = ref('')
|
||||
|
||||
// 佣金来源筛选
|
||||
const sourceFilter = ref<string | undefined>(undefined)
|
||||
const showSourcePicker = ref(false)
|
||||
|
||||
// 佣金来源选项
|
||||
const sourceOptions = [
|
||||
{ label: '全部来源', value: undefined },
|
||||
{ label: '成本价差', value: 'cost_diff' },
|
||||
{ label: '一次性佣金', value: 'one_time' },
|
||||
]
|
||||
|
||||
// 获取当前选中的来源名称
|
||||
const selectedSourceName = computed(() => {
|
||||
if (sourceFilter.value === undefined) return '佣金来源'
|
||||
const option = sourceOptions.find(o => o.value === sourceFilter.value)
|
||||
return option?.label || '佣金来源'
|
||||
})
|
||||
|
||||
// 格式化金额(分转元,带千分号)
|
||||
function formatAmount(amount: number) {
|
||||
const yuan = (amount / 100).toFixed(2)
|
||||
const parts = yuan.split('.')
|
||||
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
return `¥${parts.join('.')}`
|
||||
}
|
||||
|
||||
// 获取佣金来源文本
|
||||
function getSourceText(source: string) {
|
||||
const map: Record<string, string> = {
|
||||
cost_diff: '成本价差',
|
||||
one_time: '一次性佣金',
|
||||
tier_bonus: '梯度奖励',
|
||||
}
|
||||
return map[source] || source
|
||||
}
|
||||
|
||||
// 获取佣金状态文本
|
||||
function getStatusText(status: number) {
|
||||
const map: Record<number, string> = {
|
||||
1: '已入账',
|
||||
2: '已失效',
|
||||
}
|
||||
return map[status] || '未知'
|
||||
}
|
||||
|
||||
// 获取佣金状态颜色
|
||||
function getStatusColor(status: number) {
|
||||
const map: Record<number, { bg: string, text: string }> = {
|
||||
1: { bg: '#e8f5e9', text: '#2e7d32' }, // 已入账
|
||||
2: { bg: '#f5f5f5', text: '#999' }, // 已失效
|
||||
}
|
||||
return map[status] || { bg: '#f5f5f5', text: '#999' }
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
function formatTime(time: string) {
|
||||
if (!time) return '-'
|
||||
const date = new Date(time)
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hour = String(date.getHours()).padStart(2, '0')
|
||||
const minute = String(date.getMinutes()).padStart(2, '0')
|
||||
return `${month}-${day} ${hour}:${minute}`
|
||||
}
|
||||
|
||||
// 页面加载
|
||||
onMounted(() => {
|
||||
loadCommissionSummary()
|
||||
loadCommissionStats()
|
||||
loadCommissionRecords()
|
||||
})
|
||||
|
||||
// 加载佣金概览
|
||||
async function loadCommissionSummary() {
|
||||
try {
|
||||
const summary = await getCommissionSummary()
|
||||
commissionSummary.value = summary
|
||||
}
|
||||
catch (error) {
|
||||
console.error('加载佣金概览失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载佣金统计
|
||||
async function loadCommissionStats() {
|
||||
try {
|
||||
const stats = await getCommissionStats()
|
||||
commissionStats.value = stats
|
||||
}
|
||||
catch (error) {
|
||||
console.error('加载佣金统计失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载佣金明细
|
||||
async function loadCommissionRecords() {
|
||||
loadingRecords.value = true
|
||||
|
||||
try {
|
||||
const params: any = {
|
||||
page: page.value,
|
||||
page_size: pageSize,
|
||||
}
|
||||
|
||||
// 佣金来源筛选
|
||||
if (sourceFilter.value) {
|
||||
params.commission_source = sourceFilter.value
|
||||
}
|
||||
|
||||
// 搜索关键词(支持 ICCID、虚拟号、订单号)
|
||||
if (searchKeyword.value) {
|
||||
const keyword = searchKeyword.value.trim()
|
||||
// 根据关键词类型判断搜索字段
|
||||
if (/^\d{19,20}$/.test(keyword)) {
|
||||
// ICCID 通常是19-20位数字
|
||||
params.iccid = keyword
|
||||
}
|
||||
else if (/^\d{15}$/.test(keyword)) {
|
||||
// 虚拟号通常是15位数字
|
||||
params.virtual_no = keyword
|
||||
}
|
||||
else {
|
||||
// 订单号
|
||||
params.order_no = keyword
|
||||
}
|
||||
}
|
||||
|
||||
const response = await getCommissionRecords(params)
|
||||
|
||||
if (page.value === 1) {
|
||||
commissionRecords.value = response.items || []
|
||||
}
|
||||
else {
|
||||
commissionRecords.value.push(...(response.items || []))
|
||||
}
|
||||
|
||||
total.value = response.total || 0
|
||||
}
|
||||
catch (error) {
|
||||
console.error('加载佣金明细失败:', error)
|
||||
}
|
||||
finally {
|
||||
loadingRecords.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
function handleSearch() {
|
||||
page.value = 1
|
||||
commissionRecords.value = []
|
||||
loadCommissionRecords()
|
||||
}
|
||||
|
||||
// 清空搜索
|
||||
function clearSearch() {
|
||||
searchKeyword.value = ''
|
||||
page.value = 1
|
||||
commissionRecords.value = []
|
||||
loadCommissionRecords()
|
||||
}
|
||||
|
||||
// 来源筛选变化
|
||||
function onSourceConfirm(option: { label: string, value: any }) {
|
||||
sourceFilter.value = option.value
|
||||
page.value = 1
|
||||
commissionRecords.value = []
|
||||
loadCommissionRecords()
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
function loadMore() {
|
||||
if (commissionRecords.value.length >= total.value) {
|
||||
return
|
||||
}
|
||||
page.value++
|
||||
loadCommissionRecords()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
function refreshList() {
|
||||
page.value = 1
|
||||
commissionRecords.value = []
|
||||
loadCommissionSummary()
|
||||
loadCommissionStats()
|
||||
loadCommissionRecords()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="bg-[#fafafa] min-h-screen pb-safe">
|
||||
<!-- 加载中 -->
|
||||
<view v-if="loading" class="pt-20 center">
|
||||
<i class="i-mdi-loading animate-spin text-40px text-[#999]" />
|
||||
</view>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<view v-else class="px-4 pt-4">
|
||||
<!-- 佣金概览卡片 -->
|
||||
<view v-if="commissionSummary" class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4 mb-3">
|
||||
<view class="flex items-center justify-between mb-3">
|
||||
<view class="flex items-center gap-2">
|
||||
<i class="i-mdi-currency-cny text-24px text-[#ff6700]" />
|
||||
<text class="text-15px font-600 text-[#212121]">
|
||||
佣金概览
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="grid grid-cols-2 gap-3">
|
||||
<view class="text-center p-3 bg-[#f5f5f5] rounded-12px">
|
||||
<text class="text-11px text-[#999] block mb-1">可提现</text>
|
||||
<text class="text-18px font-700 text-[#ff6700]">
|
||||
{{ formatAmount(commissionSummary.available_commission) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="text-center p-3 bg-[#f5f5f5] rounded-12px">
|
||||
<text class="text-11px text-[#999] block mb-1">累计佣金</text>
|
||||
<text class="text-18px font-700 text-[#212121]">
|
||||
{{ formatAmount(commissionSummary.total_commission) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="text-center p-3 bg-[#f5f5f5] rounded-12px">
|
||||
<text class="text-11px text-[#999] block mb-1">冻结</text>
|
||||
<text class="text-18px font-700 text-[#999]">
|
||||
{{ formatAmount(commissionSummary.frozen_commission) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="text-center p-3 bg-[#f5f5f5] rounded-12px">
|
||||
<text class="text-11px text-[#999] block mb-1">已提现</text>
|
||||
<text class="text-18px font-700 text-[#52c41a]">
|
||||
{{ formatAmount(commissionSummary.withdrawn_commission) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 佣金统计卡片 -->
|
||||
<view v-if="commissionStats" class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4 mb-3">
|
||||
<view class="flex items-center gap-2 mb-3">
|
||||
<i class="i-mdi-chart-pie text-20px text-[#ff6700]" />
|
||||
<text class="text-15px font-600 text-[#212121]">
|
||||
佣金统计
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="space-y-2">
|
||||
<view class="flex items-center justify-between">
|
||||
<text class="text-13px text-[#666]">成本价差</text>
|
||||
<view class="flex items-center gap-2">
|
||||
<text class="text-14px font-600 text-[#212121]">
|
||||
{{ formatAmount(commissionStats.cost_diff_amount) }}
|
||||
</text>
|
||||
<text class="text-11px text-[#999]">
|
||||
{{ (commissionStats.cost_diff_percent / 10).toFixed(1) }}%
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="flex items-center justify-between">
|
||||
<text class="text-13px text-[#666]">一次性佣金</text>
|
||||
<view class="flex items-center gap-2">
|
||||
<text class="text-14px font-600 text-[#212121]">
|
||||
{{ formatAmount(commissionStats.one_time_amount) }}
|
||||
</text>
|
||||
<text class="text-11px text-[#999]">
|
||||
{{ (commissionStats.one_time_percent / 10).toFixed(1) }}%
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="pt-2 border-t-1px border-[#f5f5f5]">
|
||||
<view class="flex items-center justify-between">
|
||||
<text class="text-13px font-600 text-[#212121]">总计</text>
|
||||
<text class="text-16px font-700 text-[#ff6700]">
|
||||
{{ formatAmount(commissionStats.total_amount) }}
|
||||
</text>
|
||||
</view>
|
||||
<text class="text-11px text-[#999] mt-1 block text-right">
|
||||
共 {{ commissionStats.total_count }} 笔
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<view class="mb-3">
|
||||
<view class="bg-white rounded-12px shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden">
|
||||
<view class="flex items-center px-4 py-3 gap-2">
|
||||
<i class="i-mdi-magnify text-20px text-[#999]" />
|
||||
<input
|
||||
v-model="searchKeyword"
|
||||
class="flex-1 text-14px text-[#212121] placeholder:text-[#999]"
|
||||
placeholder="搜索 ICCID / 虚拟号 / 订单号"
|
||||
@confirm="handleSearch"
|
||||
>
|
||||
<view
|
||||
v-if="searchKeyword"
|
||||
class="ml-1 min-w-32px min-h-32px flex items-center justify-center"
|
||||
@click="clearSearch"
|
||||
>
|
||||
<i class="i-mdi-close-circle text-18px text-[#999]" />
|
||||
</view>
|
||||
<view
|
||||
class="px-4 py-2 bg-[#ff6700] text-white text-13px font-600 rounded-8px min-h-32px flex items-center justify-center"
|
||||
@click="handleSearch"
|
||||
>
|
||||
搜索
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 筛选条件 -->
|
||||
<view class="flex items-center gap-2 mb-3">
|
||||
<!-- 佣金来源筛选 -->
|
||||
<view class="flex-1 bg-white rounded-full px-4 py-2 flex items-center shadow-[0_2px_12px_rgba(0,0,0,0.06)]" @click="showSourcePicker = true">
|
||||
<text class="flex-1 text-14px text-[#212121]">{{ selectedSourceName }}</text>
|
||||
<i class="i-mdi-chevron-down text-18px text-[#999]" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 佣金明细列表 -->
|
||||
<view class="mb-3">
|
||||
<view class="flex items-center justify-between mb-2 px-2">
|
||||
<text class="text-14px font-600 text-[#212121]">
|
||||
佣金明细
|
||||
</text>
|
||||
<text class="text-12px text-[#999]">
|
||||
共 {{ total }} 条记录
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view v-if="loadingRecords && page === 1" class="pt-10 center">
|
||||
<i class="i-mdi-loading animate-spin text-40px text-[#999]" />
|
||||
</view>
|
||||
|
||||
<view v-else-if="commissionRecords.length > 0" class="space-y-2">
|
||||
<view
|
||||
v-for="record in commissionRecords"
|
||||
:key="record.id"
|
||||
class="bg-white rounded-12px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4"
|
||||
>
|
||||
<view class="flex items-center justify-between mb-2">
|
||||
<view class="flex items-center gap-2">
|
||||
<i class="i-mdi-cash-plus text-20px text-[#ff6700]" />
|
||||
<text class="text-14px font-600 text-[#212121]">
|
||||
{{ getSourceText(record.commission_source) }}
|
||||
</text>
|
||||
</view>
|
||||
<view
|
||||
:style="{
|
||||
backgroundColor: getStatusColor(record.status).bg,
|
||||
color: getStatusColor(record.status).text,
|
||||
}"
|
||||
class="px-2 py-1 rounded-6px text-11px font-600"
|
||||
>
|
||||
{{ getStatusText(record.status) }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="flex items-center justify-between">
|
||||
<text class="text-12px text-[#999]">
|
||||
{{ formatTime(record.created_at) }}
|
||||
</text>
|
||||
<text class="text-18px font-700 text-[#ff6700]">
|
||||
+{{ formatAmount(record.amount) }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view v-if="record.order_id" class="mt-2 pt-2 border-t-1px border-[#f5f5f5]">
|
||||
<text class="text-11px text-[#999]">
|
||||
订单ID: {{ record.order_id }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="commissionRecords.length < total" class="py-3 center">
|
||||
<text v-if="loadingRecords" class="text-12px text-[#999]">
|
||||
加载中...
|
||||
</text>
|
||||
<text v-else class="text-12px text-[#ff6700]" @click="loadMore">
|
||||
加载更多
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="pt-10 center flex-col">
|
||||
<i class="i-mdi-cash-remove text-60px text-[#ddd] mb-3" />
|
||||
<text class="text-14px text-[#999]">
|
||||
暂无佣金记录
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部留白 -->
|
||||
<view class="h-8" />
|
||||
</view>
|
||||
|
||||
<!-- 佣金来源选择器 -->
|
||||
<SimplePicker
|
||||
v-model:show="showSourcePicker"
|
||||
:options="sourceOptions"
|
||||
title="选择佣金来源"
|
||||
@confirm="onSourceConfirm"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.space-y-2 > view:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
</style>
|
||||
16
src/pages/agent-system/commission/index.vue
Normal file
16
src/pages/agent-system/commission/index.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
// 直接跳转到佣金中心
|
||||
onMounted(() => {
|
||||
uni.redirectTo({
|
||||
url: '/pages/agent-system/commission-center/index',
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="bg-[#fafafa] min-h-screen flex items-center justify-center">
|
||||
<text class="text-14px text-[#999]">跳转中...</text>
|
||||
</view>
|
||||
</template>
|
||||
536
src/pages/agent-system/enterprise-cards/index.vue
Normal file
536
src/pages/agent-system/enterprise-cards/index.vue
Normal file
@@ -0,0 +1,536 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { getUserInfo } from '@/api/auth'
|
||||
import { getEnterpriseCards } from '@/api/enterprise'
|
||||
import type { CardInfo } from '@/api/enterprise'
|
||||
import { getCarriers } from '@/api/carrier'
|
||||
import type { CarrierInfo } from '@/api/carrier'
|
||||
import SimplePicker from '@/components/SimplePicker.vue'
|
||||
|
||||
// 企业ID (用于调用企业接口)
|
||||
const enterpriseId = ref<number>(0)
|
||||
|
||||
// 卡片列表
|
||||
const cardList = ref<CardInfo[]>([])
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
const loadingMore = ref(false)
|
||||
|
||||
// 搜索关键词
|
||||
const searchKeyword = ref('')
|
||||
|
||||
// 状态筛选 (undefined 表示全部)
|
||||
const statusFilter = ref<number | undefined>(undefined)
|
||||
|
||||
// 运营商筛选
|
||||
const carrierFilter = ref<number | undefined>(undefined)
|
||||
const showCarrierPicker = ref(false)
|
||||
|
||||
// 运营商列表
|
||||
const carrierList = ref<CarrierInfo[]>([])
|
||||
|
||||
// 网络状态选项 (network_status: 0=停机, 1=正常)
|
||||
const statusOptions = [
|
||||
{ label: '全部', value: undefined },
|
||||
{ label: '正常', value: 1 },
|
||||
{ label: '停机', value: 0 },
|
||||
]
|
||||
|
||||
// 运营商选项
|
||||
const carrierOptions = computed(() => [
|
||||
{ label: '全部运营商', value: undefined },
|
||||
...carrierList.value.map(c => ({ label: c.carrier_name, value: c.id }))
|
||||
])
|
||||
|
||||
// 获取当前选中的运营商名称
|
||||
const selectedCarrierName = computed(() => {
|
||||
if (carrierFilter.value === undefined) return '运营商'
|
||||
const option = carrierOptions.value.find(o => o.value === carrierFilter.value)
|
||||
return option?.label || '运营商'
|
||||
})
|
||||
|
||||
// 分页参数
|
||||
const page = ref(1)
|
||||
const pageSize = 20
|
||||
const hasMore = ref(true)
|
||||
|
||||
// 直接显示卡片列表 (搜索由后端处理)
|
||||
const filteredCards = computed(() => {
|
||||
return cardList.value
|
||||
})
|
||||
|
||||
// 统计数据
|
||||
const stats = computed(() => {
|
||||
const total = cardList.value.length
|
||||
// network_status: 0=停机, 1=正常
|
||||
const active = cardList.value.filter(c => c.network_status === 1).length
|
||||
const inactive = cardList.value.filter(c => c.network_status === 0).length
|
||||
// status: 1=可用, 2=已分销, 3=已停用等
|
||||
const distributed = cardList.value.filter(c => c.status === 2).length
|
||||
|
||||
return { total, active, inactive, distributed }
|
||||
})
|
||||
|
||||
// 加载运营商列表
|
||||
async function loadCarriers() {
|
||||
try {
|
||||
const response = await getCarriers({ page: 1, size: 20 })
|
||||
carrierList.value = response?.items || []
|
||||
} catch (error) {
|
||||
console.error('加载运营商列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载
|
||||
onMounted(() => {
|
||||
loadCarriers()
|
||||
loadEnterpriseCards()
|
||||
})
|
||||
|
||||
// 加载企业卡列表
|
||||
async function loadEnterpriseCards() {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 1. 获取企业ID (优先从本地存储获取)
|
||||
if (!enterpriseId.value) {
|
||||
const userInfo = uni.getStorageSync('user_info')
|
||||
if (userInfo?.enterprise_id) {
|
||||
enterpriseId.value = userInfo.enterprise_id
|
||||
} else {
|
||||
// 如果本地存储没有,则调用接口获取
|
||||
const apiUserInfo = await getUserInfo()
|
||||
if (!apiUserInfo.enterprise_id) {
|
||||
throw new Error('未找到企业ID')
|
||||
}
|
||||
enterpriseId.value = apiUserInfo.enterprise_id
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 获取企业卡片列表
|
||||
const params: any = {
|
||||
page: page.value,
|
||||
page_size: pageSize,
|
||||
}
|
||||
// 只有选择了具体状态才传 status 参数
|
||||
if (statusFilter.value !== undefined) {
|
||||
params.status = statusFilter.value
|
||||
}
|
||||
// 运营商筛选
|
||||
if (carrierFilter.value !== undefined) {
|
||||
params.carrier_id = carrierFilter.value
|
||||
}
|
||||
// 添加搜索关键词支持 (根据接口文档)
|
||||
if (searchKeyword.value) {
|
||||
// 如果关键词看起来像ICCID (纯数字)
|
||||
if (/^\d+$/.test(searchKeyword.value)) {
|
||||
params.iccid = searchKeyword.value
|
||||
} else {
|
||||
params.virtual_no = searchKeyword.value
|
||||
}
|
||||
}
|
||||
const cardsData = await getEnterpriseCards(enterpriseId.value, params)
|
||||
|
||||
if (page.value === 1) {
|
||||
cardList.value = cardsData?.items || []
|
||||
}
|
||||
else {
|
||||
cardList.value.push(...(cardsData?.items || []))
|
||||
}
|
||||
|
||||
// 判断是否还有更多数据
|
||||
const items = cardsData?.items || []
|
||||
hasMore.value = items.length >= pageSize
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('加载企业卡列表失败:', error)
|
||||
// 如果是前端逻辑错误(非 API 错误),需要手动显示提示
|
||||
if (error instanceof Error && error.message && !error.statusCode) {
|
||||
uni.$u.toast(error.message)
|
||||
}
|
||||
// API 请求错误已由拦截器统一处理
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 查看卡片详情
|
||||
function viewCardDetail(card: any) {
|
||||
// 跳转到资产详情页
|
||||
uni.navigateTo({
|
||||
url: `/pages/agent-system/asset-search/index?iccid=${card.iccid}`,
|
||||
})
|
||||
}
|
||||
|
||||
// 获取网络状态文本 (使用后端返回的 network_status_name)
|
||||
function getNetworkStatusText(networkStatus: number, networkStatusName?: string) {
|
||||
// 优先使用后端返回的状态名称
|
||||
if (networkStatusName) {
|
||||
return networkStatusName
|
||||
}
|
||||
// 降级处理
|
||||
const map: Record<number, string> = {
|
||||
0: '停机',
|
||||
1: '正常',
|
||||
}
|
||||
return map[networkStatus] ?? '未知'
|
||||
}
|
||||
|
||||
// 获取网络状态颜色
|
||||
function getNetworkStatusColor(networkStatus: number) {
|
||||
const map: Record<number, any> = {
|
||||
1: { bg: '#e8f5e9', text: '#2e7d32' }, // 正常
|
||||
0: { bg: '#ffebee', text: '#c62828' }, // 停机
|
||||
}
|
||||
return map[networkStatus] ?? { bg: '#f5f5f5', text: '#999' }
|
||||
}
|
||||
|
||||
// 获取卡状态文本 (使用后端返回的 status_name)
|
||||
function getCardStatusText(status: number, statusName?: string) {
|
||||
// 优先使用后端返回的状态名称
|
||||
if (statusName) {
|
||||
return statusName
|
||||
}
|
||||
// 降级处理
|
||||
const map: Record<number, string> = {
|
||||
1: '可用',
|
||||
2: '已分销',
|
||||
3: '已停用',
|
||||
}
|
||||
return map[status] ?? '未知'
|
||||
}
|
||||
|
||||
// 获取运营商图标
|
||||
function getCarrierIcon(carrierName: string) {
|
||||
const map: Record<string, string> = {
|
||||
'中国移动': 'i-mdi-cellphone',
|
||||
'中国联通': 'i-mdi-signal-variant',
|
||||
'中国电信': 'i-mdi-wifi',
|
||||
}
|
||||
return map[carrierName] || 'i-mdi-sim'
|
||||
}
|
||||
|
||||
// 清空搜索
|
||||
function clearSearch() {
|
||||
searchKeyword.value = ''
|
||||
page.value = 1
|
||||
hasMore.value = true
|
||||
cardList.value = []
|
||||
loadEnterpriseCards()
|
||||
}
|
||||
|
||||
// 执行搜索
|
||||
function handleSearch() {
|
||||
page.value = 1
|
||||
hasMore.value = true
|
||||
cardList.value = []
|
||||
loadEnterpriseCards()
|
||||
}
|
||||
|
||||
// 刷新列表
|
||||
function refreshList() {
|
||||
page.value = 1
|
||||
hasMore.value = true
|
||||
cardList.value = []
|
||||
loadEnterpriseCards()
|
||||
}
|
||||
|
||||
// 状态筛选变化
|
||||
function handleStatusChange(status: number | undefined) {
|
||||
statusFilter.value = status
|
||||
page.value = 1
|
||||
hasMore.value = true
|
||||
cardList.value = []
|
||||
loadEnterpriseCards()
|
||||
}
|
||||
|
||||
// 运营商筛选变化 (SimplePicker确认时触发)
|
||||
function onCarrierConfirm(option: { label: string, value: any }) {
|
||||
carrierFilter.value = option.value
|
||||
page.value = 1
|
||||
hasMore.value = true
|
||||
cardList.value = []
|
||||
loadEnterpriseCards()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="bg-[#fafafa] min-h-screen pb-safe">
|
||||
<!-- 加载中 -->
|
||||
<view v-if="loading" class="pt-20 center">
|
||||
<i class="i-mdi-loading animate-spin text-40px text-[#999]" />
|
||||
</view>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<view v-else class="px-4 pt-4">
|
||||
<!-- 统计卡片 -->
|
||||
<view class="bg-white rounded-12px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4 mb-3">
|
||||
<view class="flex items-center justify-between">
|
||||
<view class="flex items-center gap-2">
|
||||
<i class="i-mdi-sim text-24px text-[#ff6700]" />
|
||||
<text class="text-15px font-600 text-[#212121]">
|
||||
企业授权IoT卡
|
||||
</text>
|
||||
</view>
|
||||
<view class="text-right">
|
||||
<text class="text-12px text-[#999] block">
|
||||
总计
|
||||
</text>
|
||||
<text class="text-24px font-700 text-[#ff6700]">
|
||||
{{ stats.total }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<view class="mb-3">
|
||||
<view class="bg-white rounded-12px shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden">
|
||||
<view class="flex items-center px-4 py-3 gap-2">
|
||||
<i class="i-mdi-magnify text-20px text-[#999]" />
|
||||
<input
|
||||
v-model="searchKeyword"
|
||||
class="flex-1 text-14px text-[#212121] placeholder:text-[#999]"
|
||||
placeholder="搜索 ICCID / 虚拟号 / 手机号"
|
||||
@confirm="handleSearch"
|
||||
>
|
||||
<view
|
||||
v-if="searchKeyword"
|
||||
class="ml-1 min-w-32px min-h-32px flex items-center justify-center"
|
||||
@click="clearSearch"
|
||||
>
|
||||
<i class="i-mdi-close-circle text-18px text-[#999]" />
|
||||
</view>
|
||||
<view
|
||||
class="px-4 py-2 bg-[#ff6700] text-white text-13px font-600 rounded-8px min-h-32px flex items-center justify-center"
|
||||
@click="handleSearch"
|
||||
>
|
||||
搜索
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 筛选条件 -->
|
||||
<view class="flex items-center gap-2 mb-3">
|
||||
<!-- 运营商筛选 -->
|
||||
<view class="flex-1 bg-white rounded-full px-4 py-2 flex items-center shadow-[0_2px_12px_rgba(0,0,0,0.06)]" @click="showCarrierPicker = true">
|
||||
<text class="flex-1 text-14px text-[#212121]">{{ selectedCarrierName }}</text>
|
||||
<i class="i-mdi-chevron-down text-18px text-[#999]" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 状态统计和筛选 -->
|
||||
<view class="bg-white rounded-12px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4 mb-3">
|
||||
<view class="grid grid-cols-3 gap-3 mb-3">
|
||||
<view class="text-center">
|
||||
<text class="text-11px text-[#999] block mb-1">
|
||||
正常
|
||||
</text>
|
||||
<text class="text-20px font-700 text-[#3ed268]">
|
||||
{{ stats.active }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="text-center">
|
||||
<text class="text-11px text-[#999] block mb-1">
|
||||
停机
|
||||
</text>
|
||||
<text class="text-20px font-700 text-[#ff6b6b]">
|
||||
{{ stats.inactive }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="text-center">
|
||||
<text class="text-11px text-[#999] block mb-1">
|
||||
已分销
|
||||
</text>
|
||||
<text class="text-20px font-700 text-[#ff6700]">
|
||||
{{ stats.distributed }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 状态筛选 -->
|
||||
<view class="flex items-center gap-2">
|
||||
<view
|
||||
v-for="option in statusOptions"
|
||||
:key="option.value"
|
||||
:class="[
|
||||
'flex-1 text-center py-2 rounded-8px text-13px transition-all duration-200',
|
||||
statusFilter === option.value
|
||||
? 'bg-[#ff6700] text-white font-600'
|
||||
: 'bg-[#f5f5f5] text-[#666]',
|
||||
]"
|
||||
@click="handleStatusChange(option.value)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 卡片列表 -->
|
||||
<view v-if="filteredCards.length > 0" class="space-y-3 pb-4">
|
||||
<view
|
||||
v-for="card in filteredCards"
|
||||
:key="card.id"
|
||||
class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden"
|
||||
@click="viewCardDetail(card)"
|
||||
>
|
||||
<!-- 顶部状态栏 -->
|
||||
<view class="px-4 pt-4 pb-3 border-b-1px border-[#f5f5f5]">
|
||||
<view class="flex items-center justify-between mb-2">
|
||||
<view class="flex items-center gap-2">
|
||||
<i
|
||||
:class="[
|
||||
getCarrierIcon(card.carrier_name),
|
||||
'text-24px text-[#212121]',
|
||||
]"
|
||||
/>
|
||||
<view>
|
||||
<text class="text-16px font-700 text-[#212121] block">
|
||||
{{ card.carrier_name || '-' }}
|
||||
</text>
|
||||
<text class="text-11px text-[#999]">
|
||||
{{ getCardStatusText(card.status, card.status_name) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
:style="{
|
||||
backgroundColor: getNetworkStatusColor(card.network_status).bg,
|
||||
color: getNetworkStatusColor(card.network_status).text,
|
||||
}"
|
||||
class="px-3 py-1 rounded-8px text-12px font-600"
|
||||
>
|
||||
{{ getNetworkStatusText(card.network_status, card.network_status_name) }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text class="text-12px text-[#999]">
|
||||
{{ card.package_name || '暂无套餐' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 卡片信息 -->
|
||||
<view class="px-4 py-3">
|
||||
<view class="space-y-2">
|
||||
<!-- ICCID -->
|
||||
<view class="flex items-start justify-between">
|
||||
<text class="text-12px text-[#999] w-70px">ICCID</text>
|
||||
<text class="flex-1 text-13px text-[#212121] text-right break-all">
|
||||
{{ card.iccid || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 手机号 (MSISDN) -->
|
||||
<view class="flex items-start justify-between">
|
||||
<text class="text-12px text-[#999] w-70px">手机号</text>
|
||||
<text class="flex-1 text-13px font-600 text-[#212121] text-right">
|
||||
{{ card.msisdn || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 虚拟号 -->
|
||||
<view v-if="card.virtual_no" class="flex items-start justify-between">
|
||||
<text class="text-12px text-[#999] w-70px">虚拟号</text>
|
||||
<text class="flex-1 text-13px text-[#212121] text-right">
|
||||
{{ card.virtual_no }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 使用情况 -->
|
||||
<view class="px-4 pb-4">
|
||||
<view class="bg-[#f5f5f5] rounded-12px p-3">
|
||||
<view class="flex items-center justify-between mb-2">
|
||||
<text class="text-12px text-[#999]">
|
||||
流量使用
|
||||
</text>
|
||||
<text class="text-12px font-600 text-[#212121]">
|
||||
{{ card.data_usage || '0' }} / {{ card.data_total || '0' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<view class="h-6px bg-[#e0e0e0] rounded-full overflow-hidden">
|
||||
<view
|
||||
:style="{
|
||||
width: `${(parseFloat(card.data_usage || '0') / parseFloat(card.data_total || '1')) * 100}%`,
|
||||
}"
|
||||
:class="[
|
||||
'h-full rounded-full transition-all duration-300',
|
||||
(parseFloat(card.data_usage || '0') / parseFloat(card.data_total || '1')) > 0.9
|
||||
? 'bg-[#ff6b6b]'
|
||||
: 'bg-[#3ed268]',
|
||||
]"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="flex items-center justify-between mt-2 text-11px text-[#999]">
|
||||
<text>激活: {{ card.activate_time || '-' }}</text>
|
||||
<text>到期: {{ card.expire_date || '-' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-else class="pt-20 center flex-col">
|
||||
<i class="i-mdi-sim-off text-80px text-[#ddd] mb-4" />
|
||||
<text class="text-15px text-[#999] mb-2">
|
||||
{{ searchKeyword ? '未找到匹配的卡片' : '暂无卡片数据' }}
|
||||
</text>
|
||||
<text class="text-12px text-[#ccc]">
|
||||
{{ searchKeyword ? '请尝试其他搜索关键词' : '请联系管理员添加卡片' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 底部留白 -->
|
||||
<view class="h-8" />
|
||||
</view>
|
||||
|
||||
<!-- 运营商选择器 -->
|
||||
<SimplePicker
|
||||
v-model:show="showCarrierPicker"
|
||||
:options="carrierOptions"
|
||||
title="选择运营商"
|
||||
@confirm="onCarrierConfirm"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.space-y-3 > view:not(:last-child) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.space-y-2 > view:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.break-all {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
</style>
|
||||
317
src/pages/agent-system/enterprise-devices/index.vue
Normal file
317
src/pages/agent-system/enterprise-devices/index.vue
Normal file
@@ -0,0 +1,317 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { getUserInfo } from '@/api/auth'
|
||||
import { getEnterpriseDevices } from '@/api/enterprise'
|
||||
import type { DeviceInfo } from '@/api/enterprise'
|
||||
|
||||
// 企业ID (用于调用企业接口)
|
||||
const enterpriseId = ref<number>(0)
|
||||
|
||||
// 设备列表
|
||||
const deviceList = ref<DeviceInfo[]>([])
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 搜索关键词
|
||||
const searchKeyword = ref('')
|
||||
|
||||
// 不再需要状态筛选
|
||||
|
||||
// 直接显示设备列表 (搜索由后端处理)
|
||||
const filteredDevices = computed(() => {
|
||||
return deviceList.value
|
||||
})
|
||||
|
||||
// 统计数据
|
||||
const stats = computed(() => {
|
||||
const total = deviceList.value.length
|
||||
return { total }
|
||||
})
|
||||
|
||||
// 页面加载
|
||||
onMounted(() => {
|
||||
loadEnterpriseDevices()
|
||||
})
|
||||
|
||||
// 加载企业设备列表
|
||||
async function loadEnterpriseDevices() {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 1. 获取企业ID (优先从本地存储获取)
|
||||
if (!enterpriseId.value) {
|
||||
const userInfo = uni.getStorageSync('user_info')
|
||||
if (userInfo?.enterprise_id) {
|
||||
enterpriseId.value = userInfo.enterprise_id
|
||||
} else {
|
||||
// 如果本地存储没有,则调用接口获取
|
||||
const apiUserInfo = await getUserInfo()
|
||||
if (!apiUserInfo.enterprise_id) {
|
||||
throw new Error('未找到企业ID')
|
||||
}
|
||||
enterpriseId.value = apiUserInfo.enterprise_id
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 获取企业设备列表 (支持分页和虚拟号搜索)
|
||||
const params: any = {
|
||||
page: 1,
|
||||
page_size: 100, // 暂时获取更多数据,因为没有分页加载
|
||||
}
|
||||
// 添加虚拟号搜索支持
|
||||
if (searchKeyword.value) {
|
||||
params.virtual_no = searchKeyword.value
|
||||
}
|
||||
const devicesData = await getEnterpriseDevices(enterpriseId.value, params)
|
||||
|
||||
deviceList.value = devicesData?.items || []
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('加载企业设备列表失败:', error)
|
||||
// 如果是前端逻辑错误(非 API 错误),需要手动显示提示
|
||||
if (error instanceof Error && error.message && !error.statusCode) {
|
||||
uni.$u.toast(error.message)
|
||||
}
|
||||
// API 请求错误已由拦截器统一处理
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 查看设备详情
|
||||
function viewDeviceDetail(device: any) {
|
||||
// 跳转到资产详情页
|
||||
uni.navigateTo({
|
||||
url: `/pages/agent-system/asset-search/index?virtual_no=${device.virtual_no}`,
|
||||
})
|
||||
}
|
||||
|
||||
// 获取设备型号图标
|
||||
function getDeviceModelIcon(model: string) {
|
||||
// 根据设备型号返回不同图标
|
||||
if (model?.includes('WM')) {
|
||||
return 'i-mdi-router-wireless'
|
||||
}
|
||||
return 'i-mdi-devices'
|
||||
}
|
||||
|
||||
// 格式化授权时间
|
||||
function formatAuthorizedTime(time: string) {
|
||||
if (!time) return '-'
|
||||
try {
|
||||
const date = new Date(time)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
} catch {
|
||||
return time
|
||||
}
|
||||
}
|
||||
|
||||
// 清空搜索
|
||||
function clearSearch() {
|
||||
searchKeyword.value = ''
|
||||
deviceList.value = []
|
||||
loadEnterpriseDevices()
|
||||
}
|
||||
|
||||
// 执行搜索
|
||||
function handleSearch() {
|
||||
deviceList.value = []
|
||||
loadEnterpriseDevices()
|
||||
}
|
||||
|
||||
// 刷新列表
|
||||
function refreshList() {
|
||||
deviceList.value = []
|
||||
loadEnterpriseDevices()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="bg-[#fafafa] min-h-screen pb-safe">
|
||||
<!-- 加载中 -->
|
||||
<view v-if="loading" class="pt-20 center">
|
||||
<i class="i-mdi-loading animate-spin text-40px text-[#999]" />
|
||||
</view>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<view v-else class="px-4 pt-4">
|
||||
<!-- 搜索框 -->
|
||||
<view class="mb-3">
|
||||
<view class="bg-white rounded-12px shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden">
|
||||
<view class="flex items-center px-4 py-3 gap-2">
|
||||
<i class="i-mdi-magnify text-20px text-[#999]" />
|
||||
<input
|
||||
v-model="searchKeyword"
|
||||
class="flex-1 text-14px text-[#212121] placeholder:text-[#999]"
|
||||
placeholder="搜索设备名称 / 设备型号 / 虚拟号"
|
||||
@confirm="handleSearch"
|
||||
>
|
||||
<view
|
||||
v-if="searchKeyword"
|
||||
class="ml-1 min-w-32px min-h-32px flex items-center justify-center"
|
||||
@click="clearSearch"
|
||||
>
|
||||
<i class="i-mdi-close-circle text-18px text-[#999]" />
|
||||
</view>
|
||||
<view
|
||||
class="px-4 py-2 bg-[#ff6700] text-white text-13px font-600 rounded-8px min-h-32px flex items-center justify-center"
|
||||
@click="handleSearch"
|
||||
>
|
||||
搜索
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<view class="bg-white rounded-12px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4 mb-3">
|
||||
<view class="flex items-center justify-between">
|
||||
<view class="flex items-center gap-2">
|
||||
<i class="i-mdi-devices text-24px text-[#ff6700]" />
|
||||
<text class="text-15px font-600 text-[#212121]">
|
||||
企业授权设备
|
||||
</text>
|
||||
</view>
|
||||
<view class="text-right">
|
||||
<text class="text-12px text-[#999] block">
|
||||
总计
|
||||
</text>
|
||||
<text class="text-24px font-700 text-[#ff6700]">
|
||||
{{ stats.total }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 设备列表 -->
|
||||
<view v-if="filteredDevices.length > 0" class="space-y-3 pb-4">
|
||||
<view
|
||||
v-for="device in filteredDevices"
|
||||
:key="device.virtual_no"
|
||||
class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden"
|
||||
@click="viewDeviceDetail(device)"
|
||||
>
|
||||
<!-- 顶部状态栏 -->
|
||||
<view class="px-4 pt-4 pb-3 border-b-1px border-[#f5f5f5]">
|
||||
<view class="flex items-center justify-between mb-2">
|
||||
<view class="flex items-center gap-2">
|
||||
<i
|
||||
:class="[
|
||||
getDeviceModelIcon(device.device_model),
|
||||
'text-24px text-[#212121]',
|
||||
]"
|
||||
/>
|
||||
<view>
|
||||
<text class="text-16px font-700 text-[#212121] block">
|
||||
{{ device.device_name || '-' }}
|
||||
</text>
|
||||
<text class="text-11px text-[#999]">
|
||||
{{ device.device_model || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="px-3 py-1 rounded-8px text-12px font-600 bg-[#e8f5e9] text-[#2e7d32]">
|
||||
已授权
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text class="text-12px text-[#999]">
|
||||
绑定卡数: {{ device.card_count || 0 }} 张
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 设备信息 -->
|
||||
<view class="px-4 py-3">
|
||||
<view class="space-y-2">
|
||||
<!-- 虚拟号 -->
|
||||
<view class="flex items-start justify-between">
|
||||
<text class="text-12px text-[#999] w-70px">虚拟号</text>
|
||||
<text class="flex-1 text-13px font-600 text-[#212121] text-right break-all">
|
||||
{{ device.virtual_no || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 设备型号 -->
|
||||
<view class="flex items-start justify-between">
|
||||
<text class="text-12px text-[#999] w-70px">设备型号</text>
|
||||
<text class="flex-1 text-13px text-[#212121] text-right">
|
||||
{{ device.device_model || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 绑定卡数 -->
|
||||
<view class="flex items-start justify-between">
|
||||
<text class="text-12px text-[#999] w-70px">绑定卡数</text>
|
||||
<text class="flex-1 text-13px font-600 text-[#ff6700] text-right">
|
||||
{{ device.card_count || 0 }} 张
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 授权时间 -->
|
||||
<view class="px-4 pb-4">
|
||||
<view class="bg-[#f5f5f5] rounded-12px p-3">
|
||||
<view class="flex items-center justify-between">
|
||||
<text class="text-11px text-[#999]">
|
||||
授权时间
|
||||
</text>
|
||||
<text class="text-12px font-600 text-[#212121]">
|
||||
{{ formatAuthorizedTime(device.authorized_at) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-else class="pt-20 center flex-col">
|
||||
<i class="i-mdi-devices-off text-80px text-[#ddd] mb-4" />
|
||||
<text class="text-15px text-[#999] mb-2">
|
||||
{{ searchKeyword ? '未找到匹配的设备' : '暂无设备数据' }}
|
||||
</text>
|
||||
<text class="text-12px text-[#ccc]">
|
||||
{{ searchKeyword ? '请尝试其他搜索关键词' : '请联系管理员添加设备' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 底部留白 -->
|
||||
<view class="h-8" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.space-y-3 > view:not(:last-child) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.space-y-2 > view:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
</style>
|
||||
392
src/pages/agent-system/home/index.vue
Normal file
392
src/pages/agent-system/home/index.vue
Normal file
@@ -0,0 +1,392 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { getUserInfo, logout as logoutApi } from '@/api/auth'
|
||||
import { clearToken } from '@/utils/auth'
|
||||
import type { UserInfo } from '@/api/auth'
|
||||
import { getCommissionSummary, getCommissionDailyStats } from '@/api/commission'
|
||||
import type { CommissionSummary, DailyCommissionStats } from '@/api/commission'
|
||||
import Skeleton from '@/components/Skeleton.vue'
|
||||
|
||||
// 用户信息
|
||||
const userInfo = ref<UserInfo | null>(null)
|
||||
// 佣金概览
|
||||
const commissionSummary = ref<CommissionSummary | null>(null)
|
||||
// 每日佣金统计 (最近7天)
|
||||
const dailyStats = ref<DailyCommissionStats[]>([])
|
||||
// 加载状态
|
||||
const loading = ref(true)
|
||||
const loadingCommission = ref(false)
|
||||
|
||||
// 是否为代理账号
|
||||
const isAgent = computed(() => userInfo.value?.user_type === 3)
|
||||
|
||||
// 格式化金额(分转元,带千分号)
|
||||
function formatAmount(amount: number) {
|
||||
const yuan = (amount / 100).toFixed(2)
|
||||
const parts = yuan.split('.')
|
||||
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
return `¥${parts.join('.')}`
|
||||
}
|
||||
|
||||
// 格式化日期 (MM-DD)
|
||||
function formatDate(dateStr: string) {
|
||||
const date = new Date(dateStr)
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${month}-${day}`
|
||||
}
|
||||
|
||||
// 根据用户类型动态生成菜单
|
||||
const menuItems = computed(() => {
|
||||
const menus = [
|
||||
{
|
||||
title: '资产列表',
|
||||
desc: userInfo.value?.user_type === 3 ? '代理资产列表' : '企业资产列表',
|
||||
icon: 'i-mdi-view-list',
|
||||
path: '/pages/agent-system/assets/index',
|
||||
},
|
||||
]
|
||||
|
||||
// 企业端显示授权卡和授权设备
|
||||
if (userInfo.value?.user_type !== 3) {
|
||||
menus.push(
|
||||
{
|
||||
title: '授权卡列表',
|
||||
desc: '企业授权IoT卡',
|
||||
icon: 'i-mdi-sim',
|
||||
path: '/pages/agent-system/enterprise-cards/index',
|
||||
},
|
||||
{
|
||||
title: '授权设备列表',
|
||||
desc: '企业授权设备',
|
||||
icon: 'i-mdi-devices',
|
||||
path: '/pages/agent-system/enterprise-devices/index',
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 仅代理端显示佣金和提现
|
||||
if (userInfo.value?.user_type === 3) {
|
||||
menus.push(
|
||||
{
|
||||
title: '佣金中心',
|
||||
desc: '收益统计',
|
||||
icon: 'i-mdi-wallet',
|
||||
path: '/pages/agent-system/commission-center/index',
|
||||
},
|
||||
{
|
||||
title: '提现管理',
|
||||
desc: '申请提现',
|
||||
icon: 'i-mdi-cash',
|
||||
path: '/pages/agent-system/withdraw/index',
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return menus
|
||||
})
|
||||
|
||||
// 加载佣金数据
|
||||
async function loadCommissionData() {
|
||||
if (!isAgent.value) return
|
||||
|
||||
loadingCommission.value = true
|
||||
|
||||
try {
|
||||
// 1. 加载佣金概览
|
||||
const summary = await getCommissionSummary()
|
||||
commissionSummary.value = summary
|
||||
|
||||
// 2. 加载最近7天佣金统计
|
||||
const stats = await getCommissionDailyStats({ days: 7 })
|
||||
dailyStats.value = stats || []
|
||||
}
|
||||
catch (error) {
|
||||
console.error('加载佣金数据失败:', error)
|
||||
}
|
||||
finally {
|
||||
loadingCommission.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载首页数据
|
||||
async function loadHomeData() {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 加载用户信息
|
||||
const user = await getUserInfo()
|
||||
userInfo.value = user
|
||||
|
||||
console.log('首页数据加载成功:', { user })
|
||||
|
||||
// 如果是代理账号,加载佣金数据
|
||||
if (user.user_type === 3) {
|
||||
await loadCommissionData()
|
||||
}
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('加载首页数据失败:', error)
|
||||
// 如果是前端逻辑错误(非 API 错误),需要手动显示提示
|
||||
if (error instanceof Error && error.message && !error.statusCode) {
|
||||
uni.$u.toast(error.message)
|
||||
}
|
||||
// API 请求错误已由拦截器统一处理
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转页面
|
||||
function navigateTo(path: string) {
|
||||
uni.navigateTo({ url: path })
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
function logout() {
|
||||
uni.showModal({
|
||||
title: '确认退出',
|
||||
content: '确定要退出登录吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
// 调用登出接口
|
||||
await logoutApi()
|
||||
}
|
||||
catch (error) {
|
||||
console.error('登出接口调用失败:', error)
|
||||
}
|
||||
finally {
|
||||
// 清除本地登录状态
|
||||
clearToken()
|
||||
uni.removeStorageSync('refresh_token')
|
||||
uni.removeStorageSync('user_info')
|
||||
|
||||
// 跳转到登录页
|
||||
uni.reLaunch({
|
||||
url: '/pages/common/login/index',
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadHomeData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="bg-[#fafafa] min-h-screen pb-safe">
|
||||
<!-- 加载骨架屏 -->
|
||||
<Skeleton v-if="loading" type="home" />
|
||||
|
||||
<!-- 实际内容 -->
|
||||
<view v-else>
|
||||
<!-- 顶部用户信息 -->
|
||||
<view class="bg-gradient-to-r from-[#ff6700] to-[#ff8534] px-6 pt-6 pb-5">
|
||||
<view class="flex items-center justify-between">
|
||||
<!-- 左侧用户信息 -->
|
||||
<view class="flex-1">
|
||||
<view class="flex items-center gap-2 mb-1">
|
||||
<text class="text-22px font-700 text-white">
|
||||
{{ userInfo?.username || '欢迎回来' }}
|
||||
</text>
|
||||
<view
|
||||
v-if="userInfo?.user_type === 3"
|
||||
class="px-2 py-0.5 bg-white/20 rounded-6px"
|
||||
>
|
||||
<text class="text-11px text-white font-600">代理</text>
|
||||
</view>
|
||||
<view
|
||||
v-else
|
||||
class="px-2 py-0.5 bg-white/20 rounded-6px"
|
||||
>
|
||||
<text class="text-11px text-white font-600">企业</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex items-center gap-2">
|
||||
<i class="i-mdi-phone text-14px text-white/70" />
|
||||
<text class="text-13px text-white/90">
|
||||
{{ userInfo?.phone || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 右侧头像 -->
|
||||
<view
|
||||
class="w-56px h-56px rounded-full bg-white/20 backdrop-blur center border-2px border-white/30"
|
||||
@click="navigateTo('/pages/agent-system/user-info/index')"
|
||||
>
|
||||
<text class="text-24px text-white font-700">
|
||||
{{ userInfo?.username?.substring(0, 1) || 'U' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 代理端: 佣金概览 -->
|
||||
<view v-if="isAgent && commissionSummary" class="px-4 pt-4">
|
||||
<view class="bg-gradient-to-r from-[#ff6700] to-[#ff8534] rounded-16px shadow-[0_4px_16px_rgba(255,103,0,0.3)] p-4 mb-3">
|
||||
<view class="flex items-center justify-between mb-3">
|
||||
<view>
|
||||
<text class="text-13px text-white/70 block mb-1">
|
||||
可提现佣金
|
||||
</text>
|
||||
<text class="text-32px font-700 text-white block">
|
||||
{{ formatAmount(commissionSummary.available_commission) }}
|
||||
</text>
|
||||
</view>
|
||||
<i class="i-mdi-cash-multiple text-48px text-white/20" />
|
||||
</view>
|
||||
|
||||
<view class="grid grid-cols-3 gap-3 pt-3 border-t-1px border-white/20">
|
||||
<view class="text-center">
|
||||
<text class="text-11px text-white/70 block mb-1">累计</text>
|
||||
<text class="text-14px font-600 text-white">
|
||||
{{ formatAmount(commissionSummary.total_commission) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="text-center">
|
||||
<text class="text-11px text-white/70 block mb-1">冻结</text>
|
||||
<text class="text-14px font-600 text-white">
|
||||
{{ formatAmount(commissionSummary.frozen_commission) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="text-center">
|
||||
<text class="text-11px text-white/70 block mb-1">已提现</text>
|
||||
<text class="text-14px font-600 text-white">
|
||||
{{ formatAmount(commissionSummary.withdrawn_commission) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 每日佣金趋势 -->
|
||||
<view v-if="dailyStats.length > 0" class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4 mb-3">
|
||||
<view class="flex items-center justify-between mb-3">
|
||||
<view class="flex items-center gap-2">
|
||||
<i class="i-mdi-chart-line text-20px text-[#ff6700]" />
|
||||
<text class="text-15px font-600 text-[#212121]">
|
||||
近7日佣金
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 简单的趋势条形图 -->
|
||||
<view class="space-y-2">
|
||||
<view
|
||||
v-for="stat in dailyStats"
|
||||
:key="stat.date"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<text class="text-11px text-[#999] w-40px">
|
||||
{{ formatDate(stat.date) }}
|
||||
</text>
|
||||
<view class="flex-1 h-20px bg-[#f5f5f5] rounded-full overflow-hidden relative">
|
||||
<view
|
||||
:style="{
|
||||
width: `${Math.min(100, (stat.total_amount / Math.max(...dailyStats.map(s => s.total_amount))) * 100)}%`,
|
||||
}"
|
||||
class="h-full bg-gradient-to-r from-[#ff6700] to-[#ff8534] rounded-full"
|
||||
/>
|
||||
</view>
|
||||
<text class="text-11px font-600 text-[#212121] w-60px text-right">
|
||||
{{ formatAmount(stat.total_amount) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快捷功能区 -->
|
||||
<view class="px-4 pt-4">
|
||||
<text class="text-13px text-[#999] block mb-3">快捷功能</text>
|
||||
|
||||
<!-- 功能列表 -->
|
||||
<view class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden mb-3">
|
||||
<view
|
||||
v-for="(item, index) in menuItems"
|
||||
:key="item.path"
|
||||
class="flex items-center px-4 py-4 active:bg-[#f8fafc] transition-all duration-200"
|
||||
:class="{ 'border-t-1px border-[#f5f5f5]': index > 0 }"
|
||||
@click="navigateTo(item.path)"
|
||||
>
|
||||
<view class="w-40px h-40px rounded-12px bg-[#fff3e0] center mr-3">
|
||||
<i :class="item.icon" class="text-20px text-[#ff6700]" />
|
||||
</view>
|
||||
<view class="flex-1">
|
||||
<text class="text-15px text-[#212121] font-600 block">{{ item.title }}</text>
|
||||
<text class="text-12px text-[#999] block mt-0.5">{{ item.desc }}</text>
|
||||
</view>
|
||||
<i class="i-mdi-chevron-right text-20px text-[#cbd5e1]" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 其他操作区 -->
|
||||
<view class="px-4 pt-4 pb-6">
|
||||
<text class="text-13px text-[#999] block mb-3">设置</text>
|
||||
|
||||
<view class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden">
|
||||
<!-- 个人信息 -->
|
||||
<view
|
||||
class="flex items-center px-4 py-4 active:bg-[#f8fafc] transition-all duration-200"
|
||||
@click="navigateTo('/pages/agent-system/user-info/index')"
|
||||
>
|
||||
<i class="i-mdi-account-circle text-20px text-[#ff6700] mr-3" />
|
||||
<text class="flex-1 text-15px text-[#212121]">个人信息</text>
|
||||
<i class="i-mdi-chevron-right text-20px text-[#cbd5e1]" />
|
||||
</view>
|
||||
|
||||
<!-- 修改密码 -->
|
||||
<view
|
||||
class="flex items-center px-4 py-4 border-t-1px border-[#f5f5f5] active:bg-[#f8fafc] transition-all duration-200"
|
||||
@click="navigateTo('/pages/agent-system/change-password/index')"
|
||||
>
|
||||
<i class="i-mdi-lock-reset text-20px text-[#ff6700] mr-3" />
|
||||
<text class="flex-1 text-15px text-[#212121]">修改密码</text>
|
||||
<i class="i-mdi-chevron-right text-20px text-[#cbd5e1]" />
|
||||
</view>
|
||||
|
||||
<!-- 退出登录 -->
|
||||
<view
|
||||
class="flex items-center px-4 py-4 border-t-1px border-[#f5f5f5] active:bg-[#f8fafc] transition-all duration-200"
|
||||
@click="logout"
|
||||
>
|
||||
<i class="i-mdi-logout text-20px text-[#999] mr-3" />
|
||||
<text class="flex-1 text-15px text-[#212121]">退出登录</text>
|
||||
<i class="i-mdi-chevron-right text-20px text-[#cbd5e1]" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.space-y-2 > view:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
</style>
|
||||
1512
src/pages/agent-system/index.vue
Normal file
1512
src/pages/agent-system/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
23
src/pages/agent-system/my-packages/index.vue
Normal file
23
src/pages/agent-system/my-packages/index.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="bg-[#fafafa] min-h-screen flex items-center justify-center pb-safe">
|
||||
<view class="px-6 text-center">
|
||||
<i class="i-mdi-hammer-wrench text-64px text-[#ccc] block mb-4" />
|
||||
<text class="text-18px font-600 text-[#333] block mb-2">功能开发中</text>
|
||||
<text class="text-14px text-[#999] block mb-6">
|
||||
我的套餐功能正在开发中,敬请期待
|
||||
</text>
|
||||
<view
|
||||
class="bg-[#212121] text-white rounded-full px-8 py-3 inline-block"
|
||||
@click="goBack"
|
||||
>
|
||||
<text class="text-15px">返回</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
245
src/pages/agent-system/packages/index.vue
Normal file
245
src/pages/agent-system/packages/index.vue
Normal file
@@ -0,0 +1,245 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { getEnterpriseCards } from '@/api/enterprise'
|
||||
import { getUserInfo } from '@/api/auth'
|
||||
|
||||
interface CardInfo {
|
||||
id: number
|
||||
iccid: string
|
||||
msisdn: string
|
||||
virtual_no?: string
|
||||
carrier_id: number
|
||||
carrier_name?: string
|
||||
package_name?: string
|
||||
status: number
|
||||
status_name: string
|
||||
network_status: number
|
||||
network_status_name: string
|
||||
}
|
||||
|
||||
// 企业ID
|
||||
const enterpriseId = ref<number>(0)
|
||||
// IoT卡列表
|
||||
const cardList = ref<CardInfo[]>([])
|
||||
// 加载状态
|
||||
const loading = ref(true)
|
||||
|
||||
// 套餐统计
|
||||
const packageStats = computed(() => {
|
||||
const total = cardList.value.length
|
||||
const hasPackage = cardList.value.filter(c => c.package_name).length
|
||||
const noPackage = total - hasPackage
|
||||
|
||||
return { total, hasPackage, noPackage }
|
||||
})
|
||||
|
||||
// 按套餐分组
|
||||
const packageGroups = computed(() => {
|
||||
const groups = new Map<string, CardInfo[]>()
|
||||
|
||||
cardList.value.forEach(card => {
|
||||
const pkg = card.package_name || '无套餐'
|
||||
if (!groups.has(pkg)) {
|
||||
groups.set(pkg, [])
|
||||
}
|
||||
groups.get(pkg)!.push(card)
|
||||
})
|
||||
|
||||
return Array.from(groups.entries()).map(([name, cards]) => ({
|
||||
name,
|
||||
count: cards.length,
|
||||
cards,
|
||||
}))
|
||||
})
|
||||
|
||||
// 加载数据
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 获取企业ID
|
||||
if (!enterpriseId.value) {
|
||||
const userInfo = uni.getStorageSync('user_info')
|
||||
if (userInfo?.enterprise_id) {
|
||||
enterpriseId.value = userInfo.enterprise_id
|
||||
} else {
|
||||
const apiUserInfo = await getUserInfo()
|
||||
if (!apiUserInfo.enterprise_id) {
|
||||
throw new Error('未找到企业ID')
|
||||
}
|
||||
enterpriseId.value = apiUserInfo.enterprise_id
|
||||
}
|
||||
}
|
||||
|
||||
// 获取企业卡列表
|
||||
const response = await getEnterpriseCards(enterpriseId.value)
|
||||
cardList.value = response?.items || []
|
||||
} catch (error: any) {
|
||||
console.error('加载数据失败:', error)
|
||||
if (error instanceof Error && error.message && !error.statusCode) {
|
||||
uni.$u.toast(error.message)
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 查看IoT卡详情
|
||||
function viewCardDetail(card: CardInfo) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/agent-system/asset-search/index?iccid=${card.iccid}`,
|
||||
})
|
||||
}
|
||||
|
||||
// 获取卡片状态颜色
|
||||
function getStatusColor(status: number) {
|
||||
const map: Record<number, { bg: string, text: string }> = {
|
||||
1: { bg: '#f5f5f5', text: '#999' }, // 在库
|
||||
2: { bg: '#e8f5e9', text: '#2e7d32' }, // 已分销
|
||||
3: { bg: '#ffebee', text: '#c62828' }, // 已停用
|
||||
}
|
||||
return map[status] || { bg: '#f5f5f5', text: '#999' }
|
||||
}
|
||||
|
||||
// 获取网络状态颜色
|
||||
function getNetworkStatusColor(status: number) {
|
||||
return status === 1
|
||||
? { bg: '#e8f5e9', text: '#2e7d32' }
|
||||
: { bg: '#ffebee', text: '#c62828' }
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="bg-[#fafafa] min-h-screen pb-safe">
|
||||
<!-- 顶部统计 -->
|
||||
<view class="px-4 pt-4 pb-3">
|
||||
<view class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4">
|
||||
<text class="text-16px font-700 text-[#212121] block mb-3">套餐统计</text>
|
||||
<view class="grid grid-cols-3 gap-3">
|
||||
<view class="text-center">
|
||||
<text class="text-24px font-700 text-[#212121] block">{{ packageStats.total }}</text>
|
||||
<text class="text-11px text-[#999] mt-1">总IoT卡</text>
|
||||
</view>
|
||||
<view class="text-center">
|
||||
<text class="text-24px font-700 text-[#ff6700] block">{{ packageStats.hasPackage }}</text>
|
||||
<text class="text-11px text-[#999] mt-1">已订购</text>
|
||||
</view>
|
||||
<view class="text-center">
|
||||
<text class="text-24px font-700 text-[#999] block">{{ packageStats.noPackage }}</text>
|
||||
<text class="text-11px text-[#999] mt-1">未订购</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view v-if="loading" class="pt-20 flex flex-col items-center">
|
||||
<i class="i-mdi-loading animate-spin text-40px text-[#999]" />
|
||||
</view>
|
||||
|
||||
<!-- 套餐分组列表 -->
|
||||
<view v-else class="px-4 pb-4">
|
||||
<view class="space-y-3">
|
||||
<view
|
||||
v-for="group in packageGroups"
|
||||
:key="group.name"
|
||||
class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden"
|
||||
>
|
||||
<!-- 套餐头部 -->
|
||||
<view class="px-4 py-3 bg-[#f8fafc] border-b-1px border-[#f1f5f9]">
|
||||
<view class="flex items-center justify-between">
|
||||
<view class="flex items-center gap-2">
|
||||
<i class="i-mdi-package-variant text-20px text-[#ff6700]" />
|
||||
<text class="text-15px font-600 text-[#212121]">{{ group.name }}</text>
|
||||
</view>
|
||||
<text class="text-12px text-[#999]">{{ group.count }} 张</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- IoT卡列表 -->
|
||||
<view class="px-4 py-2">
|
||||
<view
|
||||
v-for="(card, index) in group.cards"
|
||||
:key="card.id"
|
||||
class="py-3 active:bg-[#f8fafc] transition-all"
|
||||
:class="{ 'border-t-1px border-[#f1f5f9]': index > 0 }"
|
||||
@click="viewCardDetail(card)"
|
||||
>
|
||||
<view class="flex items-start justify-between">
|
||||
<view class="flex-1 min-w-0">
|
||||
<!-- ICCID -->
|
||||
<text class="text-13px text-[#212121] block mb-1 font-500">
|
||||
{{ card.iccid }}
|
||||
</text>
|
||||
|
||||
<!-- 电话号码 -->
|
||||
<view class="flex items-center gap-2 mb-1">
|
||||
<i class="i-mdi-phone text-14px text-[#999]" />
|
||||
<text class="text-12px text-[#666]">
|
||||
{{ card.msisdn }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 状态标签 -->
|
||||
<view class="flex items-center gap-2">
|
||||
<!-- 卡片状态 -->
|
||||
<view
|
||||
:style="{
|
||||
backgroundColor: getStatusColor(card.status).bg,
|
||||
color: getStatusColor(card.status).text,
|
||||
}"
|
||||
class="px-2 py-0.5 rounded text-10px font-600"
|
||||
>
|
||||
{{ card.status_name }}
|
||||
</view>
|
||||
|
||||
<!-- 网络状态 -->
|
||||
<view
|
||||
:style="{
|
||||
backgroundColor: getNetworkStatusColor(card.network_status).bg,
|
||||
color: getNetworkStatusColor(card.network_status).text,
|
||||
}"
|
||||
class="px-2 py-0.5 rounded text-10px font-600"
|
||||
>
|
||||
{{ card.network_status_name }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 右侧箭头 -->
|
||||
<view class="flex items-center ml-3">
|
||||
<i class="i-mdi-chevron-right text-20px text-[#cbd5e1]" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-if="packageGroups.length === 0" class="bg-white rounded-16px py-16 flex flex-col items-center">
|
||||
<i class="i-mdi-package-variant-closed text-64px text-[#ddd] mb-3" />
|
||||
<text class="text-15px text-[#999]">暂无套餐数据</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.space-y-3 > view:not(:last-child) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
</style>
|
||||
23
src/pages/agent-system/tags/index.vue
Normal file
23
src/pages/agent-system/tags/index.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="bg-[#fafafa] min-h-screen flex items-center justify-center pb-safe">
|
||||
<view class="px-6 text-center">
|
||||
<i class="i-mdi-hammer-wrench text-64px text-[#ccc] block mb-4" />
|
||||
<text class="text-18px font-600 text-[#333] block mb-2">功能开发中</text>
|
||||
<text class="text-14px text-[#999] block mb-6">
|
||||
标签管理功能正在开发中,敬请期待
|
||||
</text>
|
||||
<view
|
||||
class="bg-[#212121] text-white rounded-full px-8 py-3 inline-block"
|
||||
@click="goBack"
|
||||
>
|
||||
<text class="text-15px">返回</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
264
src/pages/agent-system/user-info/index.vue
Normal file
264
src/pages/agent-system/user-info/index.vue
Normal file
@@ -0,0 +1,264 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { getUserInfo, logout as logoutApi } from '@/api/auth'
|
||||
import { clearToken } from '@/utils/auth'
|
||||
import type { UserInfo } from '@/api/auth'
|
||||
|
||||
// 用户信息
|
||||
const userInfo = ref<UserInfo | null>(null)
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 用户类型文本(优先使用接口返回的 user_type_name)
|
||||
const userTypeText = computed(() => {
|
||||
if (!userInfo.value)
|
||||
return ''
|
||||
return userInfo.value.user_type_name || '未知'
|
||||
})
|
||||
|
||||
// 用户类型颜色
|
||||
const userTypeColor = computed(() => {
|
||||
if (!userInfo.value)
|
||||
return { bg: '#f5f5f5', text: '#999' }
|
||||
|
||||
const colorMap: Record<number, { bg: string, text: string }> = {
|
||||
1: { bg: '#ffebee', text: '#c62828' },
|
||||
2: { bg: '#e3f2fd', text: '#1976d2' },
|
||||
3: { bg: '#e8f5e9', text: '#2e7d32' },
|
||||
4: { bg: '#fff3e0', text: '#e65100' },
|
||||
}
|
||||
return colorMap[userInfo.value.user_type] || { bg: '#f5f5f5', text: '#999' }
|
||||
})
|
||||
|
||||
// 页面加载
|
||||
onMounted(() => {
|
||||
loadUserInfo()
|
||||
})
|
||||
|
||||
// 加载用户信息
|
||||
async function loadUserInfo() {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 调用 API 获取用户信息
|
||||
const data = await getUserInfo()
|
||||
console.log('获取用户信息成功:', data)
|
||||
userInfo.value = data
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
// 如果是前端逻辑错误(非 API 错误),需要手动显示提示
|
||||
if (error instanceof Error && error.message && !error.statusCode) {
|
||||
uni.$u.toast(error.message)
|
||||
}
|
||||
// API 请求错误已由拦截器统一处理
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 修改密码
|
||||
function changePassword() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/agent-system/change-password/index',
|
||||
})
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
function logout() {
|
||||
uni.showModal({
|
||||
title: '确认退出',
|
||||
content: '确定要退出登录吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
// 调用登出接口
|
||||
await logoutApi()
|
||||
}
|
||||
catch (error) {
|
||||
console.error('登出接口调用失败:', error)
|
||||
}
|
||||
finally {
|
||||
// 清除本地登录状态
|
||||
clearToken()
|
||||
uni.removeStorageSync('refresh_token')
|
||||
uni.removeStorageSync('user_info')
|
||||
|
||||
// 跳转到登录页
|
||||
uni.reLaunch({
|
||||
url: '/pages/common/login/index',
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 复制文本
|
||||
function copyText(text: string, label: string) {
|
||||
uni.setClipboardData({
|
||||
data: text,
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: `已复制${label}`,
|
||||
icon: 'success',
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="bg-[#fafafa] min-h-screen pb-safe">
|
||||
<!-- 顶部背景区域 -->
|
||||
<view class="bg-[#212121]">
|
||||
<!-- 用户头像卡片 -->
|
||||
<view v-if="userInfo" class="px-4 pt-5 pb-6">
|
||||
<view class="center flex-col">
|
||||
<!-- 头像 -->
|
||||
<view class="w-80px h-80px rounded-full bg-[#ffffff25] center mb-3">
|
||||
<i class="i-mdi-account text-48px text-white" />
|
||||
</view>
|
||||
|
||||
<!-- 用户名 -->
|
||||
<text class="text-20px font-700 text-white block mb-1">
|
||||
{{ userInfo.username }}
|
||||
</text>
|
||||
|
||||
<!-- 用户类型 -->
|
||||
<view
|
||||
:style="{
|
||||
backgroundColor: userTypeColor.bg,
|
||||
color: userTypeColor.text,
|
||||
}"
|
||||
class="px-3 py-1 rounded-8px text-12px font-600"
|
||||
>
|
||||
{{ userTypeText }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载中 -->
|
||||
<view v-if="loading" class="pt-20 center">
|
||||
<i class="i-mdi-loading animate-spin text-40px text-[#999]" />
|
||||
</view>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<view v-else-if="userInfo" class="px-4 pt-4">
|
||||
<!-- 个人信息 -->
|
||||
<view class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] overflow-hidden mb-3">
|
||||
<view class="px-4 py-3 border-b-1px border-[#f5f5f5]">
|
||||
<text class="text-14px font-600 text-[#212121]">
|
||||
个人信息
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="px-4 py-2">
|
||||
<!-- 用户名 -->
|
||||
<view
|
||||
class="py-3 flex items-center justify-between border-b-1px border-[#f5f5f5]"
|
||||
@click="copyText(userInfo.username, '用户名')"
|
||||
>
|
||||
<text class="text-14px text-[#999]">用户名</text>
|
||||
<view class="flex items-center gap-2">
|
||||
<text class="text-14px text-[#212121]">{{ userInfo.username }}</text>
|
||||
<i class="i-mdi-content-copy text-16px text-[#999]" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 手机号 -->
|
||||
<view
|
||||
class="py-3 flex items-center justify-between border-b-1px border-[#f5f5f5]"
|
||||
@click="copyText(userInfo.phone, '手机号')"
|
||||
>
|
||||
<text class="text-14px text-[#999]">手机号</text>
|
||||
<view class="flex items-center gap-2">
|
||||
<text class="text-14px text-[#212121]">{{ userInfo.phone }}</text>
|
||||
<i class="i-mdi-content-copy text-16px text-[#999]" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 用户类型 -->
|
||||
<view class="py-3 flex items-center justify-between">
|
||||
<text class="text-14px text-[#999]">用户类型</text>
|
||||
<view
|
||||
:style="{
|
||||
backgroundColor: userTypeColor.bg,
|
||||
color: userTypeColor.text,
|
||||
}"
|
||||
class="px-3 py-1 rounded-6px text-12px font-600"
|
||||
>
|
||||
{{ userInfo.user_type_name }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="space-y-3">
|
||||
<!-- 修改密码 -->
|
||||
<view
|
||||
class="
|
||||
bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)]
|
||||
py-3 center gap-2
|
||||
transition-all duration-200
|
||||
active:bg-[#f5f5f5]
|
||||
"
|
||||
@click="changePassword"
|
||||
>
|
||||
<i class="i-mdi-lock-reset text-18px text-[#666]" />
|
||||
<text class="text-15px font-600 text-[#666]">
|
||||
修改密码
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 退出登录 -->
|
||||
<view
|
||||
class="
|
||||
bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)]
|
||||
py-3 center gap-2
|
||||
transition-all duration-200
|
||||
active:bg-[#ffebee]
|
||||
"
|
||||
@click="logout"
|
||||
>
|
||||
<i class="i-mdi-logout text-18px text-[#c62828]" />
|
||||
<text class="text-15px font-600 text-[#c62828]">
|
||||
退出登录
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部留白 -->
|
||||
<view class="h-8" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.space-y-3 > view:not(:last-child) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
</style>
|
||||
77
src/pages/agent-system/utils.ts
Normal file
77
src/pages/agent-system/utils.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
// 套餐状态枚举
|
||||
export const packageStatus = {
|
||||
0: { name: '待生效', color: '#666', bgColor: '#f5f5f5' },
|
||||
1: { name: '生效中', color: '#212121', bgColor: '#e8e8e8' },
|
||||
2: { name: '已用完', color: '#999', bgColor: '#f5f5f5' },
|
||||
3: { name: '已过期', color: '#999', bgColor: '#f5f5f5' },
|
||||
4: { name: '已失效', color: '#999', bgColor: '#f5f5f5' },
|
||||
}
|
||||
|
||||
// 套餐类型
|
||||
export const packageTypeMap = {
|
||||
formal: '正式套餐',
|
||||
addon: '加油包',
|
||||
}
|
||||
|
||||
// 佣金来源映射
|
||||
export const commissionSourceMap = {
|
||||
cost_diff: '成本差价',
|
||||
one_time: '一次性佣金',
|
||||
tier_bonus: '层级分成',
|
||||
}
|
||||
|
||||
// 格式化流量 (MB to GB)
|
||||
export function formatDataSize(mb: number): string {
|
||||
if (mb < 1024)
|
||||
return `${mb}MB`
|
||||
return `${(mb / 1024).toFixed(1)}GB`
|
||||
}
|
||||
|
||||
// 计算使用百分比
|
||||
export function calcUsagePercent(used: number, total: number): number {
|
||||
if (total === 0)
|
||||
return 0
|
||||
return Math.min(Math.round((used / total) * 100), 100)
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
export function formatDate(dateStr: string | null): string {
|
||||
if (!dateStr)
|
||||
return '--'
|
||||
const date = new Date(dateStr)
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// 格式化日期时间
|
||||
export function formatDateTime(dateStr: string | null): string {
|
||||
if (!dateStr)
|
||||
return '--'
|
||||
const date = new Date(dateStr)
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// 计算剩余天数
|
||||
export function calcRemainingDays(expiresAt: string | null): number | null {
|
||||
if (!expiresAt)
|
||||
return null
|
||||
const now = new Date()
|
||||
const expireDate = new Date(expiresAt)
|
||||
const diff = expireDate.getTime() - now.getTime()
|
||||
return Math.ceil(diff / (1000 * 60 * 60 * 24))
|
||||
}
|
||||
|
||||
// 格式化金额 (分转元)
|
||||
export function formatAmount(cents: number): string {
|
||||
return (cents / 100).toFixed(2)
|
||||
}
|
||||
|
||||
// 获取剩余天数颜色
|
||||
export function getRemainingDaysColor(days: number | null): string {
|
||||
if (days === null)
|
||||
return '#bdbdbd'
|
||||
if (days <= 7)
|
||||
return '#f44336' // 红色
|
||||
if (days <= 30)
|
||||
return '#ff9800' // 橙色
|
||||
return '#4caf50' // 绿色
|
||||
}
|
||||
587
src/pages/agent-system/withdraw/index.vue
Normal file
587
src/pages/agent-system/withdraw/index.vue
Normal file
@@ -0,0 +1,587 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { getCommissionSummary } from '@/api/commission'
|
||||
import type { CommissionSummary } from '@/api/commission'
|
||||
import { createWithdrawal, getWithdrawalRecords } from '@/api/withdrawal'
|
||||
import type { WithdrawalRecord } from '@/api/withdrawal'
|
||||
import SimplePicker from '@/components/SimplePicker.vue'
|
||||
|
||||
// 佣金概览
|
||||
const commissionSummary = ref<CommissionSummary | null>(null)
|
||||
|
||||
// 提现金额(元)
|
||||
const withdrawAmount = ref('')
|
||||
|
||||
// 账户信息
|
||||
const accountName = ref('')
|
||||
const accountNumber = ref('')
|
||||
|
||||
// 提现记录
|
||||
const withdrawRecords = ref<WithdrawalRecord[]>([])
|
||||
|
||||
// 当前Tab
|
||||
const activeTab = ref<'apply' | 'records'>('apply')
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const loadingRecords = ref(false)
|
||||
|
||||
// 分页参数
|
||||
const page = ref(1)
|
||||
const pageSize = 20
|
||||
const total = ref(0)
|
||||
|
||||
// 状态筛选
|
||||
const statusFilter = ref<number | undefined>(undefined)
|
||||
const showStatusPicker = ref(false)
|
||||
|
||||
// 状态选项
|
||||
const statusOptions = [
|
||||
{ label: '全部状态', value: undefined },
|
||||
{ label: '审核中', value: 1 },
|
||||
{ label: '已通过', value: 2 },
|
||||
{ label: '已拒绝', value: 3 },
|
||||
{ label: '已到账', value: 4 },
|
||||
]
|
||||
|
||||
// 获取当前选中的状态名称
|
||||
const selectedStatusName = computed(() => {
|
||||
if (statusFilter.value === undefined) return '提现状态'
|
||||
const option = statusOptions.find(o => o.value === statusFilter.value)
|
||||
return option?.label || '提现状态'
|
||||
})
|
||||
|
||||
// Tab选项
|
||||
const tabs = [
|
||||
{ key: 'apply', label: '申请提现' },
|
||||
{ key: 'records', label: '提现记录' },
|
||||
]
|
||||
|
||||
// 可提现金额(分转元)
|
||||
const availableAmount = computed(() => {
|
||||
if (!commissionSummary.value) return 0
|
||||
return commissionSummary.value.available_commission / 100
|
||||
})
|
||||
|
||||
// 格式化可提现金额显示(带千分号)
|
||||
const formattedAvailableAmount = computed(() => {
|
||||
const amount = availableAmount.value.toFixed(2)
|
||||
const parts = amount.split('.')
|
||||
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
return parts.join('.')
|
||||
})
|
||||
|
||||
// 实际到账金额 (无手续费,带千分号)
|
||||
const actualAmount = computed(() => {
|
||||
const amount = parseFloat(withdrawAmount.value) || 0
|
||||
const fixed = amount.toFixed(2)
|
||||
const parts = fixed.split('.')
|
||||
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
return parts.join('.')
|
||||
})
|
||||
|
||||
// 格式化输入金额显示 (带千分号)
|
||||
const formattedWithdrawAmount = computed(() => {
|
||||
if (!withdrawAmount.value) return '0.00'
|
||||
const amount = parseFloat(withdrawAmount.value) || 0
|
||||
const fixed = amount.toFixed(2)
|
||||
const parts = fixed.split('.')
|
||||
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
return parts.join('.')
|
||||
})
|
||||
|
||||
// 格式化金额(分转元,带千分号)
|
||||
function formatAmount(amount: number) {
|
||||
const yuan = (amount / 100).toFixed(2)
|
||||
const parts = yuan.split('.')
|
||||
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
return `¥${parts.join('.')}`
|
||||
}
|
||||
|
||||
// 获取提现状态文本
|
||||
function getStatusText(status: number, statusName?: string) {
|
||||
if (statusName) return statusName
|
||||
const map: Record<number, string> = {
|
||||
1: '审核中',
|
||||
2: '已通过',
|
||||
3: '已拒绝',
|
||||
4: '已到账',
|
||||
}
|
||||
return map[status] || '未知'
|
||||
}
|
||||
|
||||
// 获取提现状态颜色
|
||||
function getStatusColor(status: number) {
|
||||
const map: Record<number, { bg: string, text: string }> = {
|
||||
1: { bg: '#fff3e0', text: '#e65100' }, // 审核中
|
||||
2: { bg: '#e8f5e9', text: '#2e7d32' }, // 已通过
|
||||
3: { bg: '#ffebee', text: '#c62828' }, // 已拒绝
|
||||
4: { bg: '#e3f2fd', text: '#1565c0' }, // 已到账
|
||||
}
|
||||
return map[status] || { bg: '#f5f5f5', text: '#999' }
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
function formatTime(time: string) {
|
||||
if (!time) return '-'
|
||||
const date = new Date(time)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hour = String(date.getHours()).padStart(2, '0')
|
||||
const minute = String(date.getMinutes()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hour}:${minute}`
|
||||
}
|
||||
|
||||
// 页面加载
|
||||
onMounted(() => {
|
||||
loadCommissionSummary()
|
||||
loadWithdrawRecords()
|
||||
})
|
||||
|
||||
// 加载佣金概览
|
||||
async function loadCommissionSummary() {
|
||||
loading.value = true
|
||||
try {
|
||||
const summary = await getCommissionSummary()
|
||||
commissionSummary.value = summary
|
||||
}
|
||||
catch (error) {
|
||||
console.error('加载佣金概览失败:', error)
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载提现记录
|
||||
async function loadWithdrawRecords() {
|
||||
loadingRecords.value = true
|
||||
|
||||
try {
|
||||
const params: any = {
|
||||
page: page.value,
|
||||
page_size: pageSize,
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if (statusFilter.value !== undefined) {
|
||||
params.status = statusFilter.value
|
||||
}
|
||||
|
||||
const response = await getWithdrawalRecords(params)
|
||||
|
||||
if (page.value === 1) {
|
||||
withdrawRecords.value = response.items || []
|
||||
}
|
||||
else {
|
||||
withdrawRecords.value.push(...(response.items || []))
|
||||
}
|
||||
|
||||
total.value = response.total || 0
|
||||
}
|
||||
catch (error) {
|
||||
console.error('加载提现记录失败:', error)
|
||||
}
|
||||
finally {
|
||||
loadingRecords.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 快捷金额按钮
|
||||
function setQuickAmount(amount: number) {
|
||||
withdrawAmount.value = amount.toString()
|
||||
}
|
||||
|
||||
// 全部提现
|
||||
function setAllAmount() {
|
||||
withdrawAmount.value = availableAmount.value.toFixed(2)
|
||||
}
|
||||
|
||||
// 提交提现申请
|
||||
async function handleSubmit() {
|
||||
// 校验
|
||||
const amount = parseFloat(withdrawAmount.value)
|
||||
if (!amount || amount <= 0) {
|
||||
uni.$u.toast('请输入提现金额')
|
||||
return
|
||||
}
|
||||
|
||||
if (amount > availableAmount.value) {
|
||||
uni.$u.toast('提现金额超过可用余额')
|
||||
return
|
||||
}
|
||||
|
||||
if (!accountName.value.trim()) {
|
||||
uni.$u.toast('请输入收款人姓名')
|
||||
return
|
||||
}
|
||||
|
||||
if (!accountNumber.value.trim()) {
|
||||
uni.$u.toast('请输入支付宝账号')
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
|
||||
try {
|
||||
const result = await createWithdrawal({
|
||||
account_name: accountName.value.trim(),
|
||||
account_number: accountNumber.value.trim(),
|
||||
amount: Math.round(amount * 100), // 元转分
|
||||
withdrawal_method: 'alipay',
|
||||
})
|
||||
|
||||
uni.$u.toast('提现申请已提交')
|
||||
|
||||
// 清空表单
|
||||
withdrawAmount.value = ''
|
||||
accountName.value = ''
|
||||
accountNumber.value = ''
|
||||
|
||||
// 刷新数据
|
||||
await loadCommissionSummary()
|
||||
page.value = 1
|
||||
await loadWithdrawRecords()
|
||||
|
||||
// 切换到记录Tab
|
||||
activeTab.value = 'records'
|
||||
}
|
||||
catch (error) {
|
||||
console.error('提现申请失败:', error)
|
||||
}
|
||||
finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 切换Tab
|
||||
function handleTabChange(tab: 'apply' | 'records') {
|
||||
activeTab.value = tab
|
||||
if (tab === 'records') {
|
||||
page.value = 1
|
||||
loadWithdrawRecords()
|
||||
}
|
||||
}
|
||||
|
||||
// 状态筛选变化
|
||||
function onStatusConfirm(option: { label: string, value: any }) {
|
||||
statusFilter.value = option.value
|
||||
page.value = 1
|
||||
withdrawRecords.value = []
|
||||
loadWithdrawRecords()
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
function loadMore() {
|
||||
if (withdrawRecords.value.length >= total.value) {
|
||||
return
|
||||
}
|
||||
page.value++
|
||||
loadWithdrawRecords()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="bg-[#fafafa] min-h-screen pb-safe">
|
||||
<!-- 可提现金额卡片 -->
|
||||
<view class="px-4 pt-4 pb-3">
|
||||
<view class="bg-gradient-to-r from-[#ff6700] to-[#ff8534] rounded-16px shadow-[0_4px_16px_rgba(255,103,0,0.3)] p-4">
|
||||
<view class="flex items-center justify-between">
|
||||
<view>
|
||||
<text class="text-13px text-white/70 block mb-1">
|
||||
可提现金额
|
||||
</text>
|
||||
<text class="text-32px font-700 text-white block">
|
||||
¥{{ formattedAvailableAmount }}
|
||||
</text>
|
||||
</view>
|
||||
<i class="i-mdi-cash-multiple text-48px text-white/20" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Tab切换 -->
|
||||
<view class="px-4 mb-3">
|
||||
<view class="bg-white rounded-12px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-1 flex">
|
||||
<view
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
:class="[
|
||||
'flex-1 text-center py-2 rounded-8px text-14px transition-all duration-200',
|
||||
activeTab === tab.key
|
||||
? 'bg-[#ff6700] text-white font-600'
|
||||
: 'text-[#666]',
|
||||
]"
|
||||
@click="handleTabChange(tab.key)"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 申请提现 -->
|
||||
<view v-if="activeTab === 'apply'" class="px-4">
|
||||
<view class="bg-white rounded-16px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4 mb-3">
|
||||
<!-- 提现金额 -->
|
||||
<view class="mb-4">
|
||||
<text class="text-13px text-[#666] block mb-2">提现金额</text>
|
||||
<view class="flex items-center border-1px border-[#e0e0e0] rounded-8px px-3 py-2">
|
||||
<text class="text-20px text-[#666] mr-1">¥</text>
|
||||
<input
|
||||
v-model="withdrawAmount"
|
||||
type="digit"
|
||||
class="flex-1 text-20px text-[#212121]"
|
||||
placeholder="0.00"
|
||||
>
|
||||
</view>
|
||||
|
||||
<!-- 快捷金额 -->
|
||||
<view class="flex items-center gap-2 mt-2">
|
||||
<view
|
||||
v-for="amount in [100, 500, 1000]"
|
||||
:key="amount"
|
||||
class="px-3 py-1 bg-[#f5f5f5] text-[#666] text-12px rounded-6px"
|
||||
@click="setQuickAmount(amount)"
|
||||
>
|
||||
{{ amount }}元
|
||||
</view>
|
||||
<view
|
||||
class="px-3 py-1 bg-[#fff3e0] text-[#ff6700] text-12px rounded-6px font-600"
|
||||
@click="setAllAmount"
|
||||
>
|
||||
全部提现
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 收款人姓名 -->
|
||||
<view class="mb-4 border-2px border-[#ff6700] rounded-16px p-4 bg-white shadow-[0_2px_12px_rgba(255,103,0,0.15)]">
|
||||
<view class="flex items-center mb-3">
|
||||
<text class="text-14px font-600 text-[#212121] mr-1">收款人姓名</text>
|
||||
<text class="text-12px text-[#ff6700]">*</text>
|
||||
</view>
|
||||
<input
|
||||
v-model="accountName"
|
||||
class="w-full text-16px text-[#212121] font-500"
|
||||
placeholder="请输入支付宝实名认证的真实姓名"
|
||||
placeholder-style="color: #ccc"
|
||||
>
|
||||
</view>
|
||||
|
||||
<!-- 支付宝账号 -->
|
||||
<view class="mb-4 border-2px border-[#ff6700] rounded-16px p-4 bg-white shadow-[0_2px_12px_rgba(255,103,0,0.15)]">
|
||||
<view class="flex items-center mb-3">
|
||||
<text class="text-14px font-600 text-[#212121] mr-1">支付宝账号</text>
|
||||
<text class="text-12px text-[#ff6700]">*</text>
|
||||
</view>
|
||||
<input
|
||||
v-model="accountNumber"
|
||||
class="w-full text-16px text-[#212121] font-500"
|
||||
placeholder="请输入支付宝账号/手机号/邮箱"
|
||||
placeholder-style="color: #ccc"
|
||||
>
|
||||
</view>
|
||||
|
||||
<!-- 费用说明 -->
|
||||
<view class="bg-gradient-to-r from-[#f5f5f5] to-[#fafafa] rounded-12px p-4 mb-4 border-1px border-[#e0e0e0]">
|
||||
<view class="flex items-center justify-between mb-3">
|
||||
<text class="text-13px text-[#666]">提现金额</text>
|
||||
<text class="text-16px font-600 text-[#212121]">¥{{ formattedWithdrawAmount }}</text>
|
||||
</view>
|
||||
<view class="pt-3 border-t-2px border-[#ff6700]">
|
||||
<view class="flex items-center justify-between">
|
||||
<view>
|
||||
<text class="text-14px font-600 text-[#212121] block">实际到账</text>
|
||||
<text class="text-11px text-[#52c41a] block mt-0.5">
|
||||
✓ 无手续费,全额到账
|
||||
</text>
|
||||
</view>
|
||||
<text class="text-24px font-700 text-[#52c41a]">¥{{ actualAmount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<view
|
||||
:class="[
|
||||
'w-full py-3 rounded-8px text-center text-15px font-600 transition-all duration-200',
|
||||
submitting ? 'bg-[#ccc] text-white' : 'bg-[#ff6700] text-white',
|
||||
]"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ submitting ? '提交中...' : '提交申请' }}
|
||||
</view>
|
||||
|
||||
<!-- 温馨提示 -->
|
||||
<view class="mt-3 p-4 bg-[#fff3e0] rounded-12px">
|
||||
<view class="flex items-start gap-2">
|
||||
<i class="i-mdi-information text-16px text-[#e65100] mt-0.5" />
|
||||
<view class="flex-1">
|
||||
<text class="text-12px font-600 text-[#e65100] block mb-2">温馨提示</text>
|
||||
<text class="text-12px text-[#666] block leading-relaxed">
|
||||
1. 目前仅支持支付宝提现<br>
|
||||
2. 无手续费,全额到账<br>
|
||||
3. 收款人姓名需与支付宝实名一致<br>
|
||||
4. 提现申请提交后将在1-3个工作日内处理
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提现记录 -->
|
||||
<view v-else class="px-4">
|
||||
<!-- 筛选条件 -->
|
||||
<view class="flex items-center gap-2 mb-3">
|
||||
<!-- 状态筛选 -->
|
||||
<view class="flex-1 bg-white rounded-full px-4 py-2 flex items-center shadow-[0_2px_12px_rgba(0,0,0,0.06)]" @click="showStatusPicker = true">
|
||||
<text class="flex-1 text-14px text-[#212121]">{{ selectedStatusName }}</text>
|
||||
<i class="i-mdi-chevron-down text-18px text-[#999]" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 记录列表 -->
|
||||
<view v-if="loadingRecords && page === 1" class="pt-10 center">
|
||||
<i class="i-mdi-loading animate-spin text-40px text-[#999]" />
|
||||
</view>
|
||||
|
||||
<view v-else-if="withdrawRecords.length > 0" class="space-y-2 mb-3">
|
||||
<view
|
||||
v-for="record in withdrawRecords"
|
||||
:key="record.id"
|
||||
class="bg-white rounded-12px shadow-[0_2px_12px_rgba(0,0,0,0.06)] p-4"
|
||||
>
|
||||
<view class="flex items-center justify-between mb-3">
|
||||
<view class="flex items-center gap-2">
|
||||
<i class="i-mdi-bank-transfer text-20px text-[#ff6700]" />
|
||||
<text class="text-14px font-600 text-[#212121]">
|
||||
{{ record.withdrawal_no }}
|
||||
</text>
|
||||
</view>
|
||||
<view
|
||||
:style="{
|
||||
backgroundColor: getStatusColor(record.status).bg,
|
||||
color: getStatusColor(record.status).text,
|
||||
}"
|
||||
class="px-2 py-1 rounded-6px text-11px font-600"
|
||||
>
|
||||
{{ getStatusText(record.status, record.status_name) }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="space-y-2">
|
||||
<view class="flex items-center justify-between mb-2">
|
||||
<text class="text-13px text-[#666]">提现金额</text>
|
||||
<text class="text-18px font-700 text-[#212121]">
|
||||
{{ formatAmount(record.amount) }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view v-if="record.fee > 0" class="flex items-center justify-between">
|
||||
<text class="text-12px text-[#999]">手续费</text>
|
||||
<text class="text-13px text-[#ff6700]">
|
||||
-{{ formatAmount(record.fee) }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="flex items-center justify-between pt-2 border-t-1px border-[#f5f5f5]">
|
||||
<view>
|
||||
<text class="text-12px text-[#999] block">实际到账</text>
|
||||
<text v-if="record.fee === 0" class="text-10px text-[#52c41a] block mt-0.5">
|
||||
无手续费
|
||||
</text>
|
||||
</view>
|
||||
<text class="text-18px font-700 text-[#52c41a]">
|
||||
{{ formatAmount(record.actual_amount) }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="pt-2 border-t-1px border-[#f5f5f5]">
|
||||
<view class="flex items-center justify-between mb-1">
|
||||
<text class="text-11px text-[#999]">收款人</text>
|
||||
<text class="text-12px text-[#212121]">
|
||||
{{ record.account_name }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="flex items-center justify-between mb-1">
|
||||
<text class="text-11px text-[#999]">支付宝账号</text>
|
||||
<text class="text-12px text-[#212121]">
|
||||
{{ record.account_number }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="flex items-center justify-between">
|
||||
<text class="text-11px text-[#999]">申请时间</text>
|
||||
<text class="text-11px text-[#999]">
|
||||
{{ formatTime(record.created_at) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 拒绝原因 -->
|
||||
<view v-if="record.status === 3 && record.reject_reason" class="pt-2 border-t-1px border-[#f5f5f5]">
|
||||
<text class="text-11px text-[#c62828] block">
|
||||
拒绝原因: {{ record.reject_reason }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="withdrawRecords.length < total" class="py-3 center">
|
||||
<text v-if="loadingRecords" class="text-12px text-[#999]">
|
||||
加载中...
|
||||
</text>
|
||||
<text v-else class="text-12px text-[#ff6700]" @click="loadMore">
|
||||
加载更多
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="pt-10 center flex-col">
|
||||
<i class="i-mdi-file-document-outline text-60px text-[#ddd] mb-3" />
|
||||
<text class="text-14px text-[#999]">
|
||||
暂无提现记录
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部留白 -->
|
||||
<view class="h-8" />
|
||||
|
||||
<!-- 状态选择器 -->
|
||||
<SimplePicker
|
||||
v-model:show="showStatusPicker"
|
||||
:options="statusOptions"
|
||||
title="选择提现状态"
|
||||
@confirm="onStatusConfirm"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.space-y-2 > view:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
</style>
|
||||
35
src/pages/common/404/index.vue
Normal file
35
src/pages/common/404/index.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="not-found">
|
||||
<u-navbar left-icon-size="40rpx" @left-click="handleBack" />
|
||||
<u-empty
|
||||
mode="page"
|
||||
text-size="20"
|
||||
text="页面不存在"
|
||||
icon="/static/images/404.png"
|
||||
width="380"
|
||||
height="380"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { HOME_PATH } from '@/router';
|
||||
|
||||
function handleBack() {
|
||||
uni.$u.route({
|
||||
type: 'switchTab',
|
||||
url: HOME_PATH,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.not-found {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
481
src/pages/common/login/index.vue
Normal file
481
src/pages/common/login/index.vue
Normal file
@@ -0,0 +1,481 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<!-- 背景装饰 -->
|
||||
<view class="bg-decor">
|
||||
<view class="shape shape-1" />
|
||||
<view class="shape shape-2" />
|
||||
<view class="shape shape-3" />
|
||||
</view>
|
||||
|
||||
<!-- 登录卡片 -->
|
||||
<view class="login-card">
|
||||
<!-- 顶部品牌区 -->
|
||||
<view class="brand-header">
|
||||
<view class="welcome-info">
|
||||
<view class="welcome-title">欢迎回来</view>
|
||||
<view class="welcome-desc">请输入您的账号密码进行登录</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view class="form-area">
|
||||
<!-- 账号输入框 -->
|
||||
<view class="input-wrap" :class="{ focused: accountFocused }">
|
||||
<input
|
||||
v-model="account"
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder="请输入账号"
|
||||
@focus="accountFocused = true"
|
||||
@blur="accountFocused = false"
|
||||
>
|
||||
</view>
|
||||
|
||||
<!-- 密码输入框 -->
|
||||
<view class="input-wrap" :class="{ focused: passwordFocused }">
|
||||
<input
|
||||
v-model="password"
|
||||
class="input"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
placeholder="请输入密码"
|
||||
@focus="passwordFocused = true"
|
||||
@blur="passwordFocused = false"
|
||||
>
|
||||
<image
|
||||
v-if="password"
|
||||
class="pwd-toggle"
|
||||
:src="showPassword ? '/static/icons/preview-open.png' : '/static/icons/preview-close-one.png'"
|
||||
mode="aspectFit"
|
||||
@tap="togglePassword"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<button
|
||||
class="btn"
|
||||
:disabled="loading"
|
||||
@tap="submit"
|
||||
>
|
||||
{{ loading ? '登录中...' : '登录' }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { HOME_PATH, isTabBarPath, LOGIN_PATH, removeQueryString } from '@/router';
|
||||
import { setToken } from '@/utils/auth';
|
||||
import { login } from '@/api/auth';
|
||||
|
||||
const account = ref<string>('');
|
||||
const password = ref<string>('');
|
||||
const accountFocused = ref<boolean>(false);
|
||||
const passwordFocused = ref<boolean>(false);
|
||||
const showPassword = ref<boolean>(false);
|
||||
const loading = ref<boolean>(false);
|
||||
let redirect = HOME_PATH;
|
||||
|
||||
const isFormValid = computed(() => {
|
||||
return account.value.trim().length > 0 && password.value.trim().length > 0;
|
||||
});
|
||||
|
||||
function togglePassword() {
|
||||
showPassword.value = !showPassword.value;
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (!isFormValid.value) {
|
||||
if (!account.value.trim()) {
|
||||
uni.$u.toast('请输入账号');
|
||||
return;
|
||||
}
|
||||
if (!password.value.trim()) {
|
||||
uni.$u.toast('请输入密码');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (loading.value) return;
|
||||
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
// 调用登录接口(禁用拦截器的自动 toast,由页面层统一处理)
|
||||
const res = await login({
|
||||
username: account.value.trim(),
|
||||
password: password.value.trim(),
|
||||
});
|
||||
|
||||
// 保存 access_token
|
||||
setToken(res.access_token);
|
||||
|
||||
// 保存 refresh_token 到本地存储
|
||||
uni.setStorageSync('refresh_token', res.refresh_token);
|
||||
|
||||
// 保存用户信息到本地存储
|
||||
uni.setStorageSync('user_info', res.user);
|
||||
|
||||
// 调试日志:查看用户信息
|
||||
console.log('登录成功,用户信息:', res.user);
|
||||
|
||||
uni.showToast({
|
||||
title: '登录成功',
|
||||
icon: 'success',
|
||||
duration: 1500,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
// 根据用户类型跳转到不同页面
|
||||
// user_type: 1=超级管理员 2=平台 3=代理 4=企业
|
||||
if (res.user.user_type === 3 || res.user.user_type === 4) {
|
||||
// 代理商和企业用户都跳转到首页(首页会根据用户类型显示不同功能)
|
||||
uni.$u.route({
|
||||
type: 'redirectTo',
|
||||
url: '/pages/agent-system/home/index',
|
||||
});
|
||||
} else {
|
||||
// 其他用户(超级管理员、平台)跳转到默认页面
|
||||
uni.$u.route({
|
||||
type: 'redirectTo',
|
||||
url: redirect,
|
||||
});
|
||||
}
|
||||
}, 1500);
|
||||
} catch (error: any) {
|
||||
console.error('登录失败:', error);
|
||||
// 拦截器已经显示了后端返回的错误信息,这里不需要再次提示
|
||||
// 如果需要额外处理,可以访问 error.data.msg 获取错误信息
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onLoad((options: any) => {
|
||||
if (options.redirect && removeQueryString(options.redirect) !== LOGIN_PATH) {
|
||||
redirect = decodeURIComponent(options.redirect);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32rpx;
|
||||
background: linear-gradient(135deg, #fafafa 0%, #f5f5f5 100%);
|
||||
overflow: hidden;
|
||||
animation: bgFade 0.8s ease-out;
|
||||
}
|
||||
|
||||
@keyframes bgFade {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 背景装饰 - 小米风格 */
|
||||
.bg-decor {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background:
|
||||
linear-gradient(90deg, transparent 0%, rgba(255, 103, 0, 0.02) 50%, transparent 100%);
|
||||
animation: scanline 8s linear infinite;
|
||||
}
|
||||
|
||||
.shape {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.shape-1 {
|
||||
width: 800rpx;
|
||||
height: 800rpx;
|
||||
top: -300rpx;
|
||||
right: -200rpx;
|
||||
background: radial-gradient(circle, rgba(255, 103, 0, 0.08), transparent 60%);
|
||||
border-radius: 50%;
|
||||
animation: pulse1 10s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.shape-2 {
|
||||
width: 600rpx;
|
||||
height: 600rpx;
|
||||
bottom: -200rpx;
|
||||
left: -150rpx;
|
||||
background: radial-gradient(circle, rgba(255, 103, 0, 0.06), transparent 60%);
|
||||
border-radius: 50%;
|
||||
animation: pulse2 12s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.shape-3 {
|
||||
width: 400rpx;
|
||||
height: 400rpx;
|
||||
top: 50%;
|
||||
right: -100rpx;
|
||||
background: radial-gradient(circle, rgba(255, 103, 0, 0.04), transparent 60%);
|
||||
border-radius: 50%;
|
||||
animation: pulse3 14s infinite ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scanline {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse1 {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse2 {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.15);
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse3 {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
/* 登录卡片 - 优化风格 */
|
||||
.login-card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 680rpx;
|
||||
padding: 60rpx 48rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08), 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
||||
z-index: 1;
|
||||
animation: cardFadeIn 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
@keyframes cardFadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(20rpx);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 顶部欢迎区 */
|
||||
.brand-header {
|
||||
margin-bottom: 64rpx;
|
||||
animation: slideInDown 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) 0.1s both;
|
||||
|
||||
.welcome-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
|
||||
.welcome-title {
|
||||
font-size: 48rpx;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(135deg, #333 0%, #666 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
letter-spacing: 1rpx;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.welcome-desc {
|
||||
font-size: 26rpx;
|
||||
font-weight: 400;
|
||||
color: #999;
|
||||
letter-spacing: 0.5rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInDown {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(-20rpx);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 表单区域 - 小米风格 */
|
||||
.form-area {
|
||||
animation: fadeInUp 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) 0.2s both;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(15rpx);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 输入框 - 柔和配色 */
|
||||
.input-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 32rpx;
|
||||
padding: 0 24rpx;
|
||||
height: 100rpx;
|
||||
background: #ffffff;
|
||||
border: 2rpx solid #e8e8e8;
|
||||
border-radius: 25rpx;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
|
||||
&.focused {
|
||||
background: #ffffff;
|
||||
border-color: #d4af37;
|
||||
box-shadow: 0 0 0 4rpx rgba(212, 175, 55, 0.08);
|
||||
transform: translateY(-2rpx);
|
||||
}
|
||||
|
||||
.input {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
line-height: normal;
|
||||
|
||||
&::placeholder {
|
||||
color: #b0b0b0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 移除 input 组件的默认边框 */
|
||||
:deep(input) {
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.pwd-toggle {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
margin-left: 16rpx;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
opacity: 0.5;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:active {
|
||||
opacity: 1;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 登录按钮 - 统一橙色 */
|
||||
.btn {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: #fff !important;
|
||||
background: #ff7c24 !important;
|
||||
border: none;
|
||||
border-radius: 25rpx;
|
||||
letter-spacing: 2rpx;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4rpx 16rpx rgba(255, 124, 36, 0.3);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
|
||||
&[disabled] {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
||||
transform: translateX(-100%);
|
||||
animation: shimmer 2.5s infinite;
|
||||
}
|
||||
|
||||
&:active:not([disabled]) {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 2rpx 10rpx rgba(255, 124, 36, 0.35);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: none;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
103
src/pages/common/theme/index.vue
Normal file
103
src/pages/common/theme/index.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<view class="theme-bg min-h-screen p-5" :class="themeClass">
|
||||
<!-- 主题模式选择 -->
|
||||
<view class="mb-5">
|
||||
<view class="theme-text-content mb-5 px-5 text-28rpx font-medium">
|
||||
主题模式
|
||||
</view>
|
||||
<view class="theme-bg-secondary overflow-hidden rounded-12rpx">
|
||||
<view
|
||||
v-for="option in themeOptions" :key="option.value"
|
||||
class="flex items-center border-b px-5 py-7.5 transition-all duration-300"
|
||||
:class="{ 'theme-bg-secondary': theme === option.value }" @click="setTheme(option.value)"
|
||||
>
|
||||
<view class="mr-5">
|
||||
<view class="text-48rpx c-primary" :class="option.icon" />
|
||||
</view>
|
||||
<view class="flex-1">
|
||||
<text class="theme-text mb-2 block text-28rpx font-medium">
|
||||
{{ option.label }}
|
||||
</text>
|
||||
<text class="theme-text-tips block text-24rpx">
|
||||
{{ option.description }}
|
||||
</text>
|
||||
</view>
|
||||
<view v-if="theme === option.value" class="ml-5">
|
||||
<u-icon name="checkmark" color="var(--theme-primary)" size="20" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 主题预览 -->
|
||||
<view class="mb-5">
|
||||
<view class="theme-text-content mb-5 px-5 text-28rpx font-medium">
|
||||
主题预览
|
||||
</view>
|
||||
<view class="theme-bg-secondary rounded-12rpx p-7.5">
|
||||
<view class="mb-7.5">
|
||||
<text class="theme-text text-28rpx font-medium">
|
||||
示例内容
|
||||
</text>
|
||||
</view>
|
||||
<view class="mb-7.5 flex flex-wrap gap-5">
|
||||
<view class="min-w-120rpx flex-1">
|
||||
<u-button type="primary" text="主要按钮" />
|
||||
</view>
|
||||
<view class="min-w-120rpx flex-1">
|
||||
<u-button type="success" text="成功按钮" />
|
||||
</view>
|
||||
<view class="min-w-120rpx flex-1">
|
||||
<u-button type="warning" text="警告按钮" />
|
||||
</view>
|
||||
<view class="min-w-120rpx flex-1">
|
||||
<u-button type="error" text="错误按钮" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex flex-col gap-2.5">
|
||||
<text class="theme-text text-28rpx font-medium">
|
||||
主要文字颜色
|
||||
</text>
|
||||
<text class="theme-text-content text-26rpx">
|
||||
内容文字颜色
|
||||
</text>
|
||||
<text class="theme-text-tips text-24rpx">
|
||||
提示文字颜色
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快速切换按钮 -->
|
||||
<view class="py-5 text-center">
|
||||
<u-button :text="isDark ? '切换到亮色主题' : '切换到暗色主题'" type="primary" @click="toggleTheme" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ThemeMode } from '@/store/modules/app/types';
|
||||
import { useTheme } from '@/hooks';
|
||||
import { useAppStore } from '@/store';
|
||||
|
||||
const { theme, isDark, setTheme, toggleTheme } = useTheme();
|
||||
|
||||
const appStore = useAppStore();
|
||||
const themeClass = computed(() => `theme-${appStore.theme}`);
|
||||
|
||||
// 主题选项
|
||||
const themeOptions = [
|
||||
{
|
||||
label: '亮色主题',
|
||||
value: 'light' as ThemeMode,
|
||||
icon: 'i-mdi-white-balance-sunny',
|
||||
description: '适合明亮环境使用',
|
||||
},
|
||||
{
|
||||
label: '暗色主题',
|
||||
value: 'dark' as ThemeMode,
|
||||
icon: 'i-mdi-moon-and-stars',
|
||||
description: '适合暗光环境使用',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
12
src/pages/common/webview/index.vue
Normal file
12
src/pages/common/webview/index.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<web-view class="h-full" :src="url" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const url = ref<string>('');
|
||||
|
||||
onLoad((options: any) => {
|
||||
if (options.url)
|
||||
url.value = decodeURIComponent(options.url);
|
||||
});
|
||||
</script>
|
||||
58
src/pages/tab/home/index.vue
Normal file
58
src/pages/tab/home/index.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<view class="min-h-screen flex flex-col items-center">
|
||||
<image class="mb-50rpx mt-200rpx h-200rpx w-200rpx" src="@/static/images/logo.png" width="200rpx" height="200rpx" />
|
||||
<view class="flex justify-center">
|
||||
<text class="font-size-36rpx">
|
||||
{{ $t('home.intro') }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="mt-100rpx flex gap-30rpx">
|
||||
<lang-select />
|
||||
<view class="cursor-pointer" @click="toGithub">
|
||||
<view class="i-mdi-github text-40rpx" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<!-- 隐私协议组件 -->
|
||||
<agree-privacy v-model="showAgreePrivacy" :disable-check-privacy="false" @agree="handleAgree" />
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// #ifdef MP-WEIXIN
|
||||
import { useShare } from '@/hooks';
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
// 分享使用示例
|
||||
const { onShareAppMessage, onShareTimeline } = useShare({
|
||||
title: '首页',
|
||||
path: 'pages/tab/home/index',
|
||||
imageUrl: '',
|
||||
});
|
||||
onShareAppMessage();
|
||||
onShareTimeline();
|
||||
// #endif
|
||||
|
||||
const title = ref<string>();
|
||||
title.value = import.meta.env.VITE_APP_TITLE;
|
||||
|
||||
const showAgreePrivacy = ref(false);
|
||||
|
||||
// 同意隐私协议
|
||||
function handleAgree() {
|
||||
console.log('同意隐私政策');
|
||||
}
|
||||
|
||||
// 打开github
|
||||
function toGithub() {
|
||||
if (window?.open) {
|
||||
window.open('https://github.com/oyjt/uniapp-vue3-template');
|
||||
}
|
||||
else {
|
||||
uni.$u.toast('请使用浏览器打开');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user