This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const content = fs.readFileSync('src/pages/agent-system/asset-search/index.vue', 'utf8')
|
const content = fs.readFileSync('src/pages/agent-system/asset-detail/index.vue', 'utf8')
|
||||||
const lines = content.split('\n')
|
const lines = content.split('\n')
|
||||||
|
|
||||||
let depth = 0
|
let depth = 0
|
||||||
|
|||||||
21
docs/权限按钮.md
Normal file
21
docs/权限按钮.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/api/auth/login 登录接口返回了权限按钮
|
||||||
|
user:{
|
||||||
|
"id": 144,
|
||||||
|
"username": "MLLxp",
|
||||||
|
"phone": "15571055000",
|
||||||
|
"user_type": 3,
|
||||||
|
"user_type_name": "代理账号",
|
||||||
|
"shop_id": 1
|
||||||
|
},
|
||||||
|
|
||||||
|
"buttons": [
|
||||||
|
"device:clear_series",
|
||||||
|
]
|
||||||
|
|
||||||
|
这个地方有返回按钮的权限
|
||||||
|
|
||||||
|
<!-- IoT卡操作按钮 -->
|
||||||
|
|
||||||
|
<!-- 设备操作按钮 -->
|
||||||
|
|
||||||
|
资产列表里面要加这个权限编码,然后判断按钮是否显示
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
### 联动范围
|
### 联动范围
|
||||||
|
|
||||||
- 资产搜索页中的同类套餐展示:`src/pages/agent-system/asset-search/index.vue`
|
- 资产详情页中的同类套餐展示:`src/pages/agent-system/asset-detail/index.vue`
|
||||||
|
|
||||||
### 本次覆盖的 UI 区块
|
### 本次覆盖的 UI 区块
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@
|
|||||||
### 关键实现要求
|
### 关键实现要求
|
||||||
|
|
||||||
- `getPackageRemainMb` 必须改为基于“当前展示的已使用值”计算
|
- `getPackageRemainMb` 必须改为基于“当前展示的已使用值”计算
|
||||||
- `asset-detail` 与 `asset-search` 不允许维护不同口径的同名逻辑
|
- `asset-detail` 页面内不同区块不允许维护不同口径的同名逻辑
|
||||||
- 格式化继续沿用现有 `formatDataSize`,本次不单独引入新的单位换算规则
|
- 格式化继续沿用现有 `formatDataSize`,本次不单独引入新的单位换算规则
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -213,7 +213,7 @@
|
|||||||
|
|
||||||
### 验收用例 4:跨页面一致性
|
### 验收用例 4:跨页面一致性
|
||||||
|
|
||||||
- 同一套餐数据在 `asset-detail` 与 `asset-search` 中展示结果一致
|
- 同一套餐数据在 `asset-detail` 页面各区块中的展示结果一致
|
||||||
- 当前生效套餐卡片与套餐列表摘要使用同一套字段口径
|
- 当前生效套餐卡片与套餐列表摘要使用同一套字段口径
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
2
env/.env.production
vendored
2
env/.env.production
vendored
@@ -2,7 +2,7 @@
|
|||||||
VITE_APP_ENV=production
|
VITE_APP_ENV=production
|
||||||
|
|
||||||
# 接口地址
|
# 接口地址
|
||||||
VITE_API_BASE_URL=https://cmp-api.boss160.cn
|
VITE_API_BASE_URL=https://cmp-api.xm-iot.cn
|
||||||
|
|
||||||
# 是否使用代理 (生产环境不使用代理)
|
# 是否使用代理 (生产环境不使用代理)
|
||||||
VITE_APP_PROXY=false
|
VITE_APP_PROXY=false
|
||||||
|
|||||||
2
env/.env.test
vendored
2
env/.env.test
vendored
@@ -2,7 +2,7 @@
|
|||||||
VITE_APP_ENV=staging
|
VITE_APP_ENV=staging
|
||||||
|
|
||||||
# 接口地址
|
# 接口地址
|
||||||
VITE_API_BASE_URL=https://cmp-api.boss160.cn
|
VITE_API_BASE_URL=https://cmp-api.xm-iot.cn
|
||||||
|
|
||||||
# 是否使用代理 (测试环境不使用代理)
|
# 是否使用代理 (测试环境不使用代理)
|
||||||
VITE_APP_PROXY=false
|
VITE_APP_PROXY=false
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Change: Update agent asset action button permissions
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
The login response already returns button-level permission codes, but the agent asset list still renders IoT card and device action buttons without using those codes. This leaves action visibility disconnected from backend authorization and makes button exposure inconsistent with the authenticated session.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Define agent asset-list behavior for consuming button permission codes returned from `POST /api/auth/login`
|
||||||
|
- Require each IoT card action button and device action button in the agent asset list to be bound to an explicit H5 permission code:
|
||||||
|
- IoT卡停机:`asset:card_stop_h5`
|
||||||
|
- IoT卡复机:`asset:card_start_h5`
|
||||||
|
- 设备停机:`asset:device_stop_h5`
|
||||||
|
- 设备复机:`asset:device_start_h5`
|
||||||
|
- Hide action buttons when the current session does not contain the corresponding permission code
|
||||||
|
- Keep asset status text and detail-page entry visible even when action buttons are hidden
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Affected specs:
|
||||||
|
- `agent-asset-list`
|
||||||
|
- Affected code:
|
||||||
|
- `src/api/auth.ts`
|
||||||
|
- `src/pages/common/login/index.vue`
|
||||||
|
- `src/pages/agent-system/assets/index.vue`
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Agent Asset Action Buttons SHALL Respect Session Button Permissions
|
||||||
|
|
||||||
|
The agent asset list SHALL use the backend-provided button permission codes from the authenticated session to determine whether IoT card and device action buttons are rendered. The page SHALL use the following H5-specific permission-code mapping:
|
||||||
|
|
||||||
|
- IoT卡停机:`asset:card_stop_h5`
|
||||||
|
- IoT卡复机:`asset:card_start_h5`
|
||||||
|
- 设备停机:`asset:device_stop_h5`
|
||||||
|
- 设备复机:`asset:device_start_h5`
|
||||||
|
|
||||||
|
#### Scenario: Login response returns button permission codes
|
||||||
|
|
||||||
|
- **WHEN** `POST /api/auth/login` succeeds
|
||||||
|
- **THEN** the frontend SHALL treat the response `buttons` array as the source of button-level permissions for the authenticated session
|
||||||
|
- **AND** the session data used by the agent asset list SHALL preserve those button permission codes
|
||||||
|
|
||||||
|
#### Scenario: Asset action has its mapped H5 permission code and the code is present
|
||||||
|
|
||||||
|
- **WHEN** an IoT card or device action button in the agent asset list has one of the mapped H5 permission codes
|
||||||
|
- **AND** that permission code exists in the current session `buttons` array
|
||||||
|
- **THEN** the corresponding action button SHALL be rendered
|
||||||
|
|
||||||
|
#### Scenario: Asset action has its mapped H5 permission code and the code is absent
|
||||||
|
|
||||||
|
- **WHEN** an IoT card or device action button in the agent asset list has one of the mapped H5 permission codes
|
||||||
|
- **AND** that permission code does not exist in the current session `buttons` array
|
||||||
|
- **THEN** the corresponding action button SHALL NOT be rendered
|
||||||
|
|
||||||
|
#### Scenario: Session includes only a subset of asset action permissions
|
||||||
|
|
||||||
|
- **WHEN** the current session `buttons` array contains permission codes for only some of the asset actions
|
||||||
|
- **THEN** only the matching asset action buttons SHALL be shown
|
||||||
|
- **AND** non-matching asset action buttons SHALL remain hidden
|
||||||
|
|
||||||
|
#### Scenario: Session includes unrelated button codes
|
||||||
|
|
||||||
|
- **WHEN** the current session `buttons` array includes unrelated button codes such as `device:clear_series`
|
||||||
|
- **THEN** no asset action button SHALL become visible unless its own mapped H5 permission code is also present
|
||||||
|
|
||||||
|
#### Scenario: Action permissions are absent but the list item remains accessible
|
||||||
|
|
||||||
|
- **WHEN** the current session does not grant any asset action button permissions for a list item
|
||||||
|
- **THEN** the asset status text SHALL still be shown
|
||||||
|
- **AND** the detail-page entry affordance for the list item SHALL remain available
|
||||||
|
- **AND** the page SHALL NOT render placeholder or disabled action buttons in place of the hidden ones
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
## 1. Spec
|
||||||
|
|
||||||
|
- [x] 1.1 Review `docs/权限按钮.md` and confirm the login response is the source of button permission codes
|
||||||
|
- [x] 1.2 Add OpenSpec proposal, tasks, and `agent-asset-list` delta spec for action-button permissions
|
||||||
|
- [x] 1.3 Validate the new change with strict OpenSpec validation
|
||||||
|
|
||||||
|
## 2. Implementation
|
||||||
|
|
||||||
|
- [x] 2.1 Extend login response typing and session persistence for the backend-provided `buttons` array
|
||||||
|
- [x] 2.2 Define explicit H5 permission-code mapping for agent asset-list IoT card and device action buttons:
|
||||||
|
- `asset:card_stop_h5`
|
||||||
|
- `asset:card_start_h5`
|
||||||
|
- `asset:device_stop_h5`
|
||||||
|
- `asset:device_start_h5`
|
||||||
|
- [x] 2.3 Gate asset-list action-button rendering by the current session button permissions
|
||||||
|
- [x] 2.4 Verify card and device action visibility for sessions with and without matching button permission codes
|
||||||
@@ -29,6 +29,5 @@ The current agent-side asset experience exposes too much low-value information,
|
|||||||
- `agent-asset-detail`
|
- `agent-asset-detail`
|
||||||
- Affected code:
|
- Affected code:
|
||||||
- `src/pages/agent-system/assets/index.vue`
|
- `src/pages/agent-system/assets/index.vue`
|
||||||
- `src/pages/agent-system/asset-search/index.vue`
|
|
||||||
- `src/pages/agent-system/asset-detail/index.vue`
|
- `src/pages/agent-system/asset-detail/index.vue`
|
||||||
- `src/api/assets.ts`
|
- `src/api/assets.ts`
|
||||||
|
|||||||
@@ -49,8 +49,7 @@ This project is a Uni-app based agent and enterprise management frontend for IoT
|
|||||||
- Asset identifiers may be ICCID or VirtualNo depending on the resource and entry point.
|
- Asset identifiers may be ICCID or VirtualNo depending on the resource and entry point.
|
||||||
- Agent-side pages currently include:
|
- Agent-side pages currently include:
|
||||||
- asset list: `src/pages/agent-system/assets/index.vue`
|
- asset list: `src/pages/agent-system/assets/index.vue`
|
||||||
- asset search/detail: `src/pages/agent-system/asset-search/index.vue`
|
- asset detail: `src/pages/agent-system/asset-detail/index.vue`
|
||||||
- secondary asset detail page: `src/pages/agent-system/asset-detail/index.vue`
|
|
||||||
- Package display and realtime status are important parts of the asset detail experience.
|
- Package display and realtime status are important parts of the asset detail experience.
|
||||||
|
|
||||||
## Important Constraints
|
## Important Constraints
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { get, post, put } from '@/utils/request'
|
|||||||
export interface LoginParams {
|
export interface LoginParams {
|
||||||
username: string
|
username: string
|
||||||
password: string
|
password: string
|
||||||
|
device?: 'h5'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -18,6 +19,7 @@ export interface LoginResponse {
|
|||||||
access_token: string
|
access_token: string
|
||||||
refresh_token: string
|
refresh_token: string
|
||||||
expires_in: number
|
expires_in: number
|
||||||
|
buttons?: string[]
|
||||||
user: {
|
user: {
|
||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
@@ -47,6 +49,7 @@ export interface UserInfo {
|
|||||||
status?: 'active' | 'inactive'
|
status?: 'active' | 'inactive'
|
||||||
create_time?: string
|
create_time?: string
|
||||||
last_login_time?: string
|
last_login_time?: string
|
||||||
|
buttons?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -62,7 +65,12 @@ export interface ChangePasswordParams {
|
|||||||
* POST /api/auth/login
|
* POST /api/auth/login
|
||||||
*/
|
*/
|
||||||
export function login(data: LoginParams) {
|
export function login(data: LoginParams) {
|
||||||
return post<LoginResponse>('/api/auth/login', { data })
|
return post<LoginResponse>('/api/auth/login', {
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
device: data.device ?? 'h5',
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -15,30 +15,6 @@
|
|||||||
"navigationStyle": "custom"
|
"navigationStyle": "custom"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"path": "pages/agent-system/my-packages/index",
|
|
||||||
"style": {
|
|
||||||
"navigationBarTitleText": "我的套餐",
|
|
||||||
"navigationBarBackgroundColor": "#ffffff",
|
|
||||||
"navigationBarTextStyle": "black"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "pages/agent-system/commission/index",
|
|
||||||
"style": {
|
|
||||||
"navigationBarTitleText": "我的佣金",
|
|
||||||
"navigationBarBackgroundColor": "#ffffff",
|
|
||||||
"navigationBarTextStyle": "black"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "pages/agent-system/packages/index",
|
|
||||||
"style": {
|
|
||||||
"navigationBarTitleText": "套餐列表",
|
|
||||||
"navigationBarBackgroundColor": "#ffffff",
|
|
||||||
"navigationBarTextStyle": "black"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"path": "pages/agent-system/assets/index",
|
"path": "pages/agent-system/assets/index",
|
||||||
"style": {
|
"style": {
|
||||||
@@ -47,22 +23,6 @@
|
|||||||
"navigationBarTextStyle": "black"
|
"navigationBarTextStyle": "black"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"path": "pages/agent-system/tags/index",
|
|
||||||
"style": {
|
|
||||||
"navigationBarTitleText": "标签管理",
|
|
||||||
"navigationBarBackgroundColor": "#ffffff",
|
|
||||||
"navigationBarTextStyle": "black"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "pages/agent-system/asset-search/index",
|
|
||||||
"style": {
|
|
||||||
"navigationBarTitleText": "资产详情",
|
|
||||||
"navigationBarBackgroundColor": "#ffffff",
|
|
||||||
"navigationBarTextStyle": "black"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"path": "pages/agent-system/asset-detail/index",
|
"path": "pages/agent-system/asset-detail/index",
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -71,6 +71,14 @@
|
|||||||
|
|
||||||
// 用户信息
|
// 用户信息
|
||||||
const userInfo = ref<UserInfo | null>(null)
|
const userInfo = ref<UserInfo | null>(null)
|
||||||
|
const sessionButtonCodes = ref<string[]>([])
|
||||||
|
|
||||||
|
const ASSET_ACTION_BUTTON_CODES = {
|
||||||
|
cardStop: 'asset:card_stop_h5',
|
||||||
|
cardStart: 'asset:card_start_h5',
|
||||||
|
deviceStop: 'asset:device_stop_h5',
|
||||||
|
deviceStart: 'asset:device_start_h5',
|
||||||
|
} as const
|
||||||
|
|
||||||
// 获取单卡列表(代理端)
|
// 获取单卡列表(代理端)
|
||||||
function getStandaloneCards(params : any) {
|
function getStandaloneCards(params : any) {
|
||||||
@@ -130,10 +138,36 @@
|
|||||||
const carrierList = ref<CarrierInfo[]>([])
|
const carrierList = ref<CarrierInfo[]>([])
|
||||||
const isAgent = computed(() => userInfo.value?.user_type === 3)
|
const isAgent = computed(() => userInfo.value?.user_type === 3)
|
||||||
const searchPlaceholder = computed(() => activeTab.value === 'card' ? '搜索ICCID' : '搜索设备号')
|
const searchPlaceholder = computed(() => activeTab.value === 'card' ? '搜索ICCID' : '搜索设备号')
|
||||||
|
const canShowDeviceStopButton = computed(() => hasActionButtonPermission(ASSET_ACTION_BUTTON_CODES.deviceStop))
|
||||||
|
const canShowDeviceStartButton = computed(() => hasActionButtonPermission(ASSET_ACTION_BUTTON_CODES.deviceStart))
|
||||||
|
|
||||||
function hasStatusCode(error : unknown) {
|
function hasStatusCode(error : unknown) {
|
||||||
return typeof error === 'object' && error !== null && 'statusCode' in error
|
return typeof error === 'object' && error !== null && 'statusCode' in error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSessionButtonCodes() {
|
||||||
|
const cachedUserInfo = uni.getStorageSync('user_info')
|
||||||
|
|
||||||
|
if (!cachedUserInfo || typeof cachedUserInfo !== 'object' || !Array.isArray((cachedUserInfo as UserInfo).buttons)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return (cachedUserInfo as UserInfo).buttons!.filter((code) => {
|
||||||
|
return typeof code === 'string' && code.trim().length > 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasActionButtonPermission(code : string) {
|
||||||
|
return sessionButtonCodes.value.includes(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
function canShowCardStopButton(card : CardInfo) {
|
||||||
|
return card.network_status !== 0 && hasActionButtonPermission(ASSET_ACTION_BUTTON_CODES.cardStop)
|
||||||
|
}
|
||||||
|
|
||||||
|
function canShowCardStartButton(card : CardInfo) {
|
||||||
|
return card.network_status !== 1 && hasActionButtonPermission(ASSET_ACTION_BUTTON_CODES.cardStart)
|
||||||
|
}
|
||||||
// 运营商选择器选项
|
// 运营商选择器选项
|
||||||
const carrierOptions = computed(() => [
|
const carrierOptions = computed(() => [
|
||||||
{ label: '全部运营商', value: undefined },
|
{ label: '全部运营商', value: undefined },
|
||||||
@@ -410,12 +444,12 @@
|
|||||||
if (asset.type === 'card') {
|
if (asset.type === 'card') {
|
||||||
const card = asset.raw as CardInfo
|
const card = asset.raw as CardInfo
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages/agent-system/asset-search/index?iccid=${card.iccid}`,
|
url: `/pages/agent-system/asset-detail/index?iccid=${card.iccid}`,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const device = asset.raw as DeviceInfo
|
const device = asset.raw as DeviceInfo
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages/agent-system/asset-search/index?virtual_no=${device.virtual_no}`,
|
url: `/pages/agent-system/asset-detail/index?virtual_no=${device.virtual_no}`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -575,6 +609,7 @@
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 1. 获取状态栏高度
|
// 1. 获取状态栏高度
|
||||||
statusBarHeight.value = uni.getSystemInfoSync().statusBarHeight || 20
|
statusBarHeight.value = uni.getSystemInfoSync().statusBarHeight || 20
|
||||||
|
sessionButtonCodes.value = getSessionButtonCodes()
|
||||||
|
|
||||||
// 2. 加载用户信息(判断是企业端还是代理端)
|
// 2. 加载用户信息(判断是企业端还是代理端)
|
||||||
try {
|
try {
|
||||||
@@ -646,8 +681,7 @@
|
|||||||
<view v-if="isAgent"
|
<view v-if="isAgent"
|
||||||
class="flex items-center gap-1.5 px-3 py-1.5 bg-[#f8fafc] rounded-full flex-shrink-0 max-w-36"
|
class="flex items-center gap-1.5 px-3 py-1.5 bg-[#f8fafc] rounded-full flex-shrink-0 max-w-36"
|
||||||
@click="showShopPicker = true">
|
@click="showShopPicker = true">
|
||||||
<text class="text-12px truncate"
|
<text class="text-12px truncate" :class="selectedShopFilter ? 'text-brand font-medium' : 'text-[#64748b]'">
|
||||||
:class="selectedShopFilter ? 'text-brand font-medium' : 'text-[#64748b]'">
|
|
||||||
{{ selectedShopName }}
|
{{ selectedShopName }}
|
||||||
</text>
|
</text>
|
||||||
<i class="i-mdi-chevron-down text-12px text-[#94a3b8] flex-shrink-0" />
|
<i class="i-mdi-chevron-down text-12px text-[#94a3b8] flex-shrink-0" />
|
||||||
@@ -716,42 +750,30 @@
|
|||||||
<view class="flex items-center gap-2">
|
<view class="flex items-center gap-2">
|
||||||
<!-- IoT卡操作按钮 -->
|
<!-- IoT卡操作按钮 -->
|
||||||
<template v-if="asset.type === 'card'">
|
<template v-if="asset.type === 'card'">
|
||||||
<template v-if="(asset.raw as CardInfo).network_status === 0">
|
<view
|
||||||
<view
|
v-if="canShowCardStopButton(asset.raw as CardInfo)"
|
||||||
class="px-3 py-1.5 rounded-10px text-12px font-medium bg-[#52c41a] text-white shadow-sm shadow-[#52c41a]/30 active:scale-95 transition-transform"
|
class="px-3 py-1.5 rounded-10px text-12px font-medium bg-[#ff4d4f] text-white shadow-sm shadow-[#ff4d4f]/30 active:scale-95 transition-transform"
|
||||||
@click="handleStartCard(asset, $event)">
|
@click="handleStopCard(asset, $event)">
|
||||||
复机
|
停机
|
||||||
</view>
|
</view>
|
||||||
</template>
|
<view
|
||||||
<template v-else-if="(asset.raw as CardInfo).network_status === 1">
|
v-if="canShowCardStartButton(asset.raw as CardInfo)"
|
||||||
<view
|
class="px-3 py-1.5 rounded-10px text-12px font-medium bg-[#52c41a] text-white shadow-sm shadow-[#52c41a]/30 active:scale-95 transition-transform"
|
||||||
class="px-3 py-1.5 rounded-10px text-12px font-medium bg-[#ff4d4f] text-white shadow-sm shadow-[#ff4d4f]/30 active:scale-95 transition-transform"
|
@click="handleStartCard(asset, $event)">
|
||||||
@click="handleStopCard(asset, $event)">
|
复机
|
||||||
停机
|
</view>
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<view
|
|
||||||
class="px-3 py-1.5 rounded-10px text-12px font-medium bg-[#ff4d4f] text-white shadow-sm shadow-[#ff4d4f]/30 active:scale-95 transition-transform"
|
|
||||||
@click="handleStopCard(asset, $event)">
|
|
||||||
停机
|
|
||||||
</view>
|
|
||||||
<view
|
|
||||||
class="px-3 py-1.5 rounded-10px text-12px font-medium bg-[#52c41a] text-white shadow-sm shadow-[#52c41a]/30 active:scale-95 transition-transform"
|
|
||||||
@click="handleStartCard(asset, $event)">
|
|
||||||
复机
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 设备操作按钮 -->
|
<!-- 设备操作按钮 -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<view
|
<view
|
||||||
|
v-if="canShowDeviceStopButton"
|
||||||
class="px-3 py-1.5 rounded-10px text-12px font-medium bg-[#ff4d4f] text-white shadow-sm shadow-[#ff4d4f]/30 active:scale-95 transition-transform"
|
class="px-3 py-1.5 rounded-10px text-12px font-medium bg-[#ff4d4f] text-white shadow-sm shadow-[#ff4d4f]/30 active:scale-95 transition-transform"
|
||||||
@click="handleStopDevice(asset, $event)">
|
@click="handleStopDevice(asset, $event)">
|
||||||
停机
|
停机
|
||||||
</view>
|
</view>
|
||||||
<view
|
<view
|
||||||
|
v-if="canShowDeviceStartButton"
|
||||||
class="px-3 py-1.5 rounded-10px text-12px font-medium bg-[#52c41a] text-white shadow-sm shadow-[#52c41a]/30 active:scale-95 transition-transform"
|
class="px-3 py-1.5 rounded-10px text-12px font-medium bg-[#52c41a] text-white shadow-sm shadow-[#52c41a]/30 active:scale-95 transition-transform"
|
||||||
@click="handleStartDevice(asset, $event)">
|
@click="handleStartDevice(asset, $event)">
|
||||||
复机
|
复机
|
||||||
@@ -766,11 +788,10 @@
|
|||||||
<!-- 加载更多 -->
|
<!-- 加载更多 -->
|
||||||
<view v-if="activeTabHasMore" class="py-4 flex justify-center">
|
<view v-if="activeTabHasMore" class="py-4 flex justify-center">
|
||||||
<view
|
<view
|
||||||
class="min-w-32 px-5 py-2.5 rounded-full text-sm font-medium transition-all"
|
class="min-w-32 px-5 py-2.5 rounded-full text-sm font-medium transition-all flex items-center justify-center"
|
||||||
:class="loading
|
:class="loading
|
||||||
? 'bg-[#f1f5f9] text-[#94a3b8]'
|
? 'bg-[#f1f5f9] text-[#94a3b8]'
|
||||||
: 'bg-brand text-white shadow-sm shadow-brand/30 active:scale-95'"
|
: 'bg-brand text-white shadow-sm shadow-brand/30 active:scale-95'" @click="!loading && loadMore()">
|
||||||
@click="!loading && loadMore()">
|
|
||||||
<template v-if="loading">
|
<template v-if="loading">
|
||||||
<i class="i-mdi-loading animate-spin text-base mr-1.5" />
|
<i class="i-mdi-loading animate-spin text-base mr-1.5" />
|
||||||
<text>加载中...</text>
|
<text>加载中...</text>
|
||||||
|
|||||||
@@ -261,7 +261,6 @@ function loadMore() {
|
|||||||
</view>
|
</view>
|
||||||
<view class="flex items-center justify-between text-12px text-[#999]">
|
<view class="flex items-center justify-between text-12px text-[#999]">
|
||||||
<text>{{ formatTime(record.created_at) }}</text>
|
<text>{{ formatTime(record.created_at) }}</text>
|
||||||
<text v-if="record.order_id">订单 {{ record.order_id }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -54,7 +54,7 @@ const stats = computed(() => {
|
|||||||
|
|
||||||
async function loadCarriers() {
|
async function loadCarriers() {
|
||||||
try {
|
try {
|
||||||
const response = await getCarriers({ page: 1, size: 50 })
|
const response = await getCarriers({ page: 1, size: 100 })
|
||||||
carrierList.value = response?.items || []
|
carrierList.value = response?.items || []
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载运营商列表失败:', error)
|
console.error('加载运营商列表失败:', error)
|
||||||
@@ -128,7 +128,7 @@ async function loadEnterpriseCards(isRefresh = false) {
|
|||||||
|
|
||||||
function viewCardDetail(card: any) {
|
function viewCardDetail(card: any) {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages/agent-system/asset-search/index?iccid=${card.iccid}`,
|
url: `/pages/agent-system/asset-detail/index?iccid=${card.iccid}`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ async function loadEnterpriseDevices(isRefresh = false) {
|
|||||||
|
|
||||||
function viewDeviceDetail(device: any) {
|
function viewDeviceDetail(device: any) {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages/agent-system/asset-search/index?virtual_no=${device.virtual_no}`,
|
url: `/pages/agent-system/asset-detail/index?virtual_no=${device.virtual_no}`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -94,8 +94,6 @@
|
|||||||
shopId.value = user.shop_id || loginUserInfo?.shop_id || 0
|
shopId.value = user.shop_id || loginUserInfo?.shop_id || 0
|
||||||
const fundSummaryUsername = loginUserInfo?.username || user.username
|
const fundSummaryUsername = loginUserInfo?.username || user.username
|
||||||
|
|
||||||
console.log('首页数据加载成功:', { user })
|
|
||||||
|
|
||||||
// 如果是代理账号,加载佣金数据
|
// 如果是代理账号,加载佣金数据
|
||||||
if (user.user_type === 3) {
|
if (user.user_type === 3) {
|
||||||
await loadCommissionData(fundSummaryUsername)
|
await loadCommissionData(fundSummaryUsername)
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
<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-[var(--brand-primary)] 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-[var(--brand-primary)]" />
|
|
||||||
<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>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -43,7 +43,6 @@ async function loadUserInfo() {
|
|||||||
try {
|
try {
|
||||||
// 调用 API 获取用户信息
|
// 调用 API 获取用户信息
|
||||||
const data = await getUserInfo()
|
const data = await getUserInfo()
|
||||||
console.log('获取用户信息成功:', data)
|
|
||||||
userInfo.value = data
|
userInfo.value = data
|
||||||
}
|
}
|
||||||
catch (error: any) {
|
catch (error: any) {
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
// 套餐状态枚举
|
|
||||||
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' // 绿色
|
|
||||||
}
|
|
||||||
@@ -106,6 +106,10 @@ async function submit() {
|
|||||||
username: account.value.trim(),
|
username: account.value.trim(),
|
||||||
password: password.value.trim(),
|
password: password.value.trim(),
|
||||||
});
|
});
|
||||||
|
const sessionUser = {
|
||||||
|
...res.user,
|
||||||
|
buttons: Array.isArray(res.buttons) ? res.buttons : [],
|
||||||
|
};
|
||||||
|
|
||||||
// 保存 access_token
|
// 保存 access_token
|
||||||
setToken(res.access_token);
|
setToken(res.access_token);
|
||||||
@@ -113,12 +117,6 @@ async function submit() {
|
|||||||
// 保存 refresh_token 到本地存储
|
// 保存 refresh_token 到本地存储
|
||||||
uni.setStorageSync('refresh_token', res.refresh_token);
|
uni.setStorageSync('refresh_token', res.refresh_token);
|
||||||
|
|
||||||
// 保存用户信息到本地存储
|
|
||||||
uni.setStorageSync('user_info', res.user);
|
|
||||||
|
|
||||||
// 调试日志:查看用户信息
|
|
||||||
console.log('登录成功,用户信息:', res.user);
|
|
||||||
|
|
||||||
// 验证用户类型 (只允许 3=代理 和 4=企业 登录)
|
// 验证用户类型 (只允许 3=代理 和 4=企业 登录)
|
||||||
if (res.user.user_type !== 3 && res.user.user_type !== 4) {
|
if (res.user.user_type !== 3 && res.user.user_type !== 4) {
|
||||||
const typeNames: Record<number, string> = {
|
const typeNames: Record<number, string> = {
|
||||||
@@ -139,9 +137,12 @@ async function submit() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保存用户信息到本地存储
|
||||||
|
uni.setStorageSync('user_info', sessionUser);
|
||||||
|
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '登录成功',
|
title: '登录成功',
|
||||||
icon: 'success',
|
icon: 'none',
|
||||||
duration: 1500,
|
duration: 1500,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { providerType, UserState } from './types';
|
|||||||
import type { LoginReq } from '@/api/user/types';
|
import type { LoginReq } from '@/api/user/types';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { UserApi } from '@/api';
|
import { UserApi } from '@/api';
|
||||||
import { refreshToken } from '@/api/auth';
|
import { refreshToken, type UserInfo } from '@/api/auth';
|
||||||
|
|
||||||
import { clearToken, setToken } from '@/utils/auth';
|
import { clearToken, setToken } from '@/utils/auth';
|
||||||
|
|
||||||
@@ -82,12 +82,22 @@ const useUserStore = defineStore('user', {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await refreshToken(refresh_token);
|
const res = await refreshToken(refresh_token);
|
||||||
|
const rawUserInfo = uni.getStorageSync('user_info');
|
||||||
|
const cachedUserInfo = rawUserInfo && typeof rawUserInfo === 'object'
|
||||||
|
? rawUserInfo as Partial<UserInfo>
|
||||||
|
: {};
|
||||||
|
const cachedButtons = Array.isArray(cachedUserInfo.buttons) ? cachedUserInfo.buttons : [];
|
||||||
|
const sessionUser = {
|
||||||
|
...cachedUserInfo,
|
||||||
|
...res.user,
|
||||||
|
buttons: Array.isArray(res.buttons) ? res.buttons : cachedButtons,
|
||||||
|
};
|
||||||
// 更新 access_token
|
// 更新 access_token
|
||||||
setToken(res.access_token);
|
setToken(res.access_token);
|
||||||
// 更新 refresh_token
|
// 更新 refresh_token
|
||||||
uni.setStorageSync('refresh_token', res.refresh_token);
|
uni.setStorageSync('refresh_token', res.refresh_token);
|
||||||
// 更新用户信息
|
// 更新用户信息
|
||||||
uni.setStorageSync('user_info', res.user);
|
uni.setStorageSync('user_info', sessionUser);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export function setupRequest() {
|
|||||||
export function request<T = any>(config: HttpRequestConfig): Promise<T> {
|
export function request<T = any>(config: HttpRequestConfig): Promise<T> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.request(config).then((res: HttpResponse<IResponse<T>>) => {
|
http.request(config).then((res: HttpResponse<IResponse<T>>) => {
|
||||||
console.log('[ res ] >', res);
|
// console.log('[ res ] >', res);
|
||||||
const { data } = res.data;
|
const { data } = res.data;
|
||||||
resolve(data as T);
|
resolve(data as T);
|
||||||
}).catch((err: any) => {
|
}).catch((err: any) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user