fix: 新增代理系列授权-建议售价使用套餐
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m38s

This commit is contained in:
sexygoat
2026-06-12 10:41:37 +08:00
parent d23e896f97
commit e04a283319
98 changed files with 4279 additions and 6170 deletions

1
components.d.ts vendored
View File

@@ -14,7 +14,6 @@ declare module 'vue' {
ArtBreadcrumb: typeof import('./src/components/core/layouts/art-breadcrumb/index.vue')['default'] ArtBreadcrumb: typeof import('./src/components/core/layouts/art-breadcrumb/index.vue')['default']
ArtButtonMore: typeof import('./src/components/core/forms/ArtButtonMore.vue')['default'] ArtButtonMore: typeof import('./src/components/core/forms/ArtButtonMore.vue')['default']
ArtButtonTable: typeof import('./src/components/core/forms/ArtButtonTable.vue')['default'] ArtButtonTable: typeof import('./src/components/core/forms/ArtButtonTable.vue')['default']
ArtCardBanner: typeof import('./src/components/core/banners/ArtCardBanner.vue')['default']
ArtChatWindow: typeof import('./src/components/core/layouts/art-chat-window/index.vue')['default'] ArtChatWindow: typeof import('./src/components/core/layouts/art-chat-window/index.vue')['default']
ArtCutterImg: typeof import('./src/components/core/media/ArtCutterImg.vue')['default'] ArtCutterImg: typeof import('./src/components/core/media/ArtCutterImg.vue')['default']
ArtDataListCard: typeof import('./src/components/core/cards/ArtDataListCard.vue')['default'] ArtDataListCard: typeof import('./src/components/core/cards/ArtDataListCard.vue')['default']

View File

@@ -5,6 +5,7 @@
当前后台“资产信息”页还没有消费接口新增的 `gateway_extend` 字段,因此运营人员无法在资产详情中直接看到运营商侧实际停机原因。 当前后台“资产信息”页还没有消费接口新增的 `gateway_extend` 字段,因此运营人员无法在资产详情中直接看到运营商侧实际停机原因。
最新接口契约已经补充两类返回: 最新接口契约已经补充两类返回:
- 设备资产的绑定卡列表项新增 `gateway_extend` - 设备资产的绑定卡列表项新增 `gateway_extend`
- 卡资产详情新增 `gateway_extend` - 卡资产详情新增 `gateway_extend`

View File

@@ -33,15 +33,19 @@
## Decisions ## Decisions
- Decision: 能力名称使用 `payment-configuration-management`,而不是沿用页面路径中的 `wechat-config` - Decision: 能力名称使用 `payment-configuration-management`,而不是沿用页面路径中的 `wechat-config`
- Rationale: 该能力已经覆盖微信、小程序、公众号、支付宝和富友相关字段,业务边界明显大于“微信配置”。 - Rationale: 该能力已经覆盖微信、小程序、公众号、支付宝和富友相关字段,业务边界明显大于“微信配置”。
- Decision: `provider_type` 在本次 spec 中严格按文档限定为 `wechat``wechat_v2``fuiou` - Decision: `provider_type` 在本次 spec 中严格按文档限定为 `wechat``wechat_v2``fuiou`
- Rationale: 这是文档明确枚举出的渠道主类型spec 不应凭猜测新增 `alipay` - Rationale: 这是文档明确枚举出的渠道主类型spec 不应凭猜测新增 `alipay`
- Decision: `ali_*` 字段在本次 spec 中被视为支付配置对象支持的可选扩展字段,但不单独引入新的 `provider_type` - Decision: `ali_*` 字段在本次 spec 中被视为支付配置对象支持的可选扩展字段,但不单独引入新的 `provider_type`
- Rationale: 文档已经明确给出这些字段;在没有更正枚举之前,先把它们作为统一字段模型的一部分记录下来。 - Rationale: 文档已经明确给出这些字段;在没有更正枚举之前,先把它们作为统一字段模型的一部分记录下来。
- Decision: 所有读取响应中的敏感信息都必须采用脱敏或配置状态标记,不能返回明文密钥 - Decision: 所有读取响应中的敏感信息都必须采用脱敏或配置状态标记,不能返回明文密钥
- Rationale: 文档的详情和当前生效配置示例已经给出了 `***``已配置``未配置` 这类语义。 - Rationale: 文档的详情和当前生效配置示例已经给出了 `***``已配置``未配置` 这类语义。
- Decision: 更新接口采用部分更新语义,只要求提交实际需要修改的字段 - Decision: 更新接口采用部分更新语义,只要求提交实际需要修改的字段
@@ -50,6 +54,7 @@
## Risks / Trade-offs ## Risks / Trade-offs
- 风险: 当前前端类型定义尚未覆盖 `ali_*` 字段,按 spec 对齐后会触发类型和页面联动修改。 - 风险: 当前前端类型定义尚未覆盖 `ali_*` 字段,按 spec 对齐后会触发类型和页面联动修改。
- Mitigation: 实施时先调整 `src/types/api/wechatConfig.ts`,再逐步修改列表、表单和详情展示。 - Mitigation: 实施时先调整 `src/types/api/wechatConfig.ts`,再逐步修改列表、表单和详情展示。
- 风险: 文档没有给出“如何切换当前生效配置”的写接口,只有 `is_active` 状态和 `/active` 查询接口。 - 风险: 文档没有给出“如何切换当前生效配置”的写接口,只有 `is_active` 状态和 `/active` 查询接口。

View File

@@ -5,6 +5,7 @@
当前后台设备管理和企业设备授权流程还没有完整接入新的设备激活状态字段,导致运营人员无法按“已激活 / 未激活”筛选设备,也无法在资产详情中直接看到设备激活状态名称。 当前后台设备管理和企业设备授权流程还没有完整接入新的设备激活状态字段,导致运营人员无法按“已激活 / 未激活”筛选设备,也无法在资产详情中直接看到设备激活状态名称。
最新接口契约补充了以下内容: 最新接口契约补充了以下内容:
- 设备列表接口新增可选过滤参数 `activation_status` - 设备列表接口新增可选过滤参数 `activation_status`
- 设备列表与资产解析相关响应新增 `activation_status_name` - 设备列表与资产解析相关响应新增 `activation_status_name`
- 企业设备授权弹窗中的可选设备列表也需要沿用这套激活状态筛选与展示规则 - 企业设备授权弹窗中的可选设备列表也需要沿用这套激活状态筛选与展示规则

View File

@@ -26,7 +26,7 @@
```typescript ```typescript
export enum ExchangeFlowType { export enum ExchangeFlowType {
SHIPPING = 'shipping', // 物流换货 SHIPPING = 'shipping', // 物流换货
DIRECT = 'direct' // 直接换货 DIRECT = 'direct' // 直接换货
} }
export const EXCHANGE_FLOW_TYPE_MAP = { export const EXCHANGE_FLOW_TYPE_MAP = {
@@ -47,6 +47,7 @@ export const EXCHANGE_FLOW_TYPE_MAP = {
**Rationale**: 符合 Element Plus 表单最佳实践,避免不必要的字段提交。 **Rationale**: 符合 Element Plus 表单最佳实践,避免不必要的字段提交。
**Alternatives considered**: **Alternatives considered**:
- 使用 `v-show` 隐藏:会导致隐藏字段仍参与表单验证 - 使用 `v-show` 隐藏:会导致隐藏字段仍参与表单验证
- 使用动态表单项:增加复杂度,不够直观 - 使用动态表单项:增加复杂度,不够直观
@@ -55,16 +56,16 @@ export const EXCHANGE_FLOW_TYPE_MAP = {
在详情页组件中定义计算属性: 在详情页组件中定义计算属性:
```typescript ```typescript
const canShip = computed(() => const canShip = computed(
exchange.value.flow_type === 'shipping' && exchange.value.status === 2 () => exchange.value.flow_type === 'shipping' && exchange.value.status === 2
) )
const canComplete = computed(() => const canComplete = computed(
exchange.value.flow_type === 'shipping' && exchange.value.status === 3 () => exchange.value.flow_type === 'shipping' && exchange.value.status === 3
) )
const canCancel = computed(() => const canCancel = computed(
exchange.value.flow_type === 'shipping' && [1, 2].includes(exchange.value.status) () => exchange.value.flow_type === 'shipping' && [1, 2].includes(exchange.value.status)
) )
``` ```
@@ -78,6 +79,7 @@ const canCancel = computed(() =>
- **原因**: direct 类型这些字段为 null显示空白区块或"不适用"会造成页面冗余 - **原因**: direct 类型这些字段为 null显示空白区块或"不适用"会造成页面冗余
**Alternatives considered**: **Alternatives considered**:
- 显示"不适用"占位:增加不必要的视觉噪音 - 显示"不适用"占位:增加不必要的视觉噪音
- 显示空区块:用户体验不佳 - 显示空区块:用户体验不佳
@@ -105,6 +107,7 @@ const flowType = exchange.flow_type || 'shipping'
引入流程类型后,详情页的条件渲染逻辑增多。 引入流程类型后,详情页的条件渲染逻辑增多。
**Mitigation**: **Mitigation**:
- 使用计算属性封装权限判断逻辑 - 使用计算属性封装权限判断逻辑
- 添加单元测试覆盖不同流程类型的渲染场景 - 添加单元测试覆盖不同流程类型的渲染场景

View File

@@ -1,45 +0,0 @@
import request from '@/utils/http'
import { PaginationResponse, BaseResponse } from '@/types/api'
import { ArticleType, ArticleCategoryType, ArticleQueryParams } from '@/api/modules'
// 文章
export class ArticleService {
// 获取文章列表
static getArticleList(params: ArticleQueryParams) {
const { page, size, searchVal, year } = params
return request.get<PaginationResponse<ArticleType>>({
url: `/api/articles/${page}/${size}?title=${searchVal}&year=${year}`
})
}
// 获取文章类型
static getArticleTypes(params: object) {
return request.get<BaseResponse<ArticleCategoryType[]>>({
url: '/api/articles/types',
params
})
}
// 获取文章详情
static getArticleDetail(id: number) {
return request.get<BaseResponse<ArticleType>>({
url: `/api/articles/${id}`
})
}
// 新增文章
static addArticle(params: any) {
return request.post<BaseResponse>({
url: '/api/articles/',
data: params
})
}
// 编辑文章
static editArticle(id: number, params: any) {
return request.put<BaseResponse>({
url: `/api/articles/${id}`,
data: params
})
}
}

View File

@@ -1,25 +0,0 @@
import { asyncRoutes } from '@/router/routes/asyncRoutes'
import { menuDataToRouter } from '@/router/utils/menuToRouter'
import { AppRouteRecord } from '@/types/router'
interface MenuResponse {
menuList: AppRouteRecord[]
}
// 菜单接口
export const menuService = {
async getMenuList(delay = 300): Promise<MenuResponse> {
try {
// 模拟接口返回的菜单数据
const menuData = asyncRoutes
// 处理菜单数据
const menuList = menuData.map((route) => menuDataToRouter(route))
// 模拟接口延迟
await new Promise((resolve) => setTimeout(resolve, delay))
return { menuList }
} catch (error) {
throw error instanceof Error ? error : new Error('获取菜单失败')
}
}
}

View File

@@ -1,72 +0,0 @@
/**
* 文章相关类型定义
*/
// 文章类型 (新命名规范)
export interface Article {
id?: number
blogClass: string
title: string
count?: number
htmlContent: string
createTime: string
homeImg: string
brief: string
typeName?: string
status?: number
author?: string
tags?: string[]
}
// 兼容原有的文章类型命名
export interface ArticleType {
id?: number
blog_class: string
title: string
count?: number
html_content: string
create_time: string
home_img: string
brief: string
type_name?: string
}
// 文章分类类型 (新命名规范)
export interface ArticleCategory {
id: number
name: string
icon: string
count: number
description?: string
sortOrder?: number
}
// 兼容原有的文章分类类型命名
export interface ArticleCategoryType {
id: number
name: string
icon: string
count: number
}
// 文章查询参数
export interface ArticleQueryParams {
page?: number
size?: number
searchVal?: string
year?: string
categoryId?: number
status?: number
}
// 文章创建/更新参数
export interface ArticleFormData {
blogClass: string
title: string
htmlContent: string
homeImg: string
brief: string
author?: string
tags?: string[]
status?: number
}

View File

@@ -17,6 +17,7 @@ import type {
SuspendResumeRecord, SuspendResumeRecord,
CardOrder, CardOrder,
BaseResponse, BaseResponse,
PaginationParams,
PaginationResponse, PaginationResponse,
ListResponse, ListResponse,
GatewayRealnameLinkResponse, GatewayRealnameLinkResponse,
@@ -24,12 +25,36 @@ import type {
ImportIotCardResponse, ImportIotCardResponse,
IotCardImportTask, IotCardImportTask,
IotCardImportTaskDetail, IotCardImportTaskDetail,
IotCardImportTaskQueryParams,
StandaloneCardQueryParams, StandaloneCardQueryParams,
StandaloneIotCard, StandaloneIotCard,
AllocateStandaloneCardsRequest,
AllocateStandaloneCardsResponse,
RecallStandaloneCardsRequest,
AssetAllocationRecordQueryParams,
AssetAllocationRecord,
AssetAllocationRecordDetail,
BatchSetCardSeriesBindingRequest, BatchSetCardSeriesBindingRequest,
BatchSetCardSeriesBindingResponse BatchSetCardSeriesBindingResponse
} from '@/types/api' } from '@/types/api'
type ApiQueryParams = PaginationParams & Record<string, unknown>
interface ImportFailureRecord {
line?: number
row?: number
iccid?: string
reason: string
}
interface CardChangeNotice {
id: string | number
iccids: string[]
reason: string
createTime?: string
operatorName?: string
}
export class CardService extends BaseService { export class CardService extends BaseService {
// ========== 号卡商品管理 ========== // ========== 号卡商品管理 ==========
@@ -37,7 +62,7 @@ export class CardService extends BaseService {
* 获取号卡商品列表 * 获取号卡商品列表
* @param params 查询参数 * @param params 查询参数
*/ */
static getSimCardProducts(params?: any): Promise<PaginationResponse<SimCardProduct>> { static getSimCardProducts(params?: ApiQueryParams): Promise<PaginationResponse<SimCardProduct>> {
return this.getPage<SimCardProduct>('/api/simcard-products', params) return this.getPage<SimCardProduct>('/api/simcard-products', params)
} }
@@ -95,15 +120,6 @@ export class CardService extends BaseService {
return this.getOne<Card>(`/api/cards/iccid/${iccid}`) return this.getOne<Card>(`/api/cards/iccid/${iccid}`)
} }
/**
* 通过ICCID查询单卡详情旧接口已废弃
* @deprecated 使用 AssetService.resolveAsset 替代
* @param iccid ICCID
*/
static getIotCardDetailByIccid(iccid: string): Promise<BaseResponse<any>> {
return this.getOne<any>(`/api/admin/iot-cards/by-iccid/${iccid}`)
}
/** /**
* 网卡操作(充值、停复机、增减流量等) * 网卡操作(充值、停复机、增减流量等)
* @param params 操作参数 * @param params 操作参数
@@ -214,7 +230,7 @@ export class CardService extends BaseService {
* 获取导入批次列表 * 获取导入批次列表
* @param params 查询参数 * @param params 查询参数
*/ */
static getImportBatches(params?: any): Promise<PaginationResponse<CardImportBatch>> { static getImportBatches(params?: ApiQueryParams): Promise<PaginationResponse<CardImportBatch>> {
return this.getPage<CardImportBatch>('/api/cards/import-batches', params) return this.getPage<CardImportBatch>('/api/cards/import-batches', params)
} }
@@ -223,7 +239,7 @@ export class CardService extends BaseService {
* @param file Excel文件 * @param file Excel文件
* @param params 额外参数 * @param params 额外参数
*/ */
static importCards(file: File, params?: Record<string, any>): Promise<BaseResponse> { static importCards(file: File, params?: Record<string, unknown>): Promise<BaseResponse> {
return this.upload('/api/cards/import', file, params) return this.upload('/api/cards/import', file, params)
} }
@@ -231,15 +247,17 @@ export class CardService extends BaseService {
* 获取导入失败记录 * 获取导入失败记录
* @param batchId 批次ID * @param batchId 批次ID
*/ */
static getImportFailures(batchId: string | number): Promise<ListResponse<any>> { static getImportFailures(batchId: string | number): Promise<ListResponse<ImportFailureRecord>> {
return this.getList(`/api/cards/import-batches/${batchId}/failures`) return this.getList<ImportFailureRecord>(`/api/cards/import-batches/${batchId}/failures`)
} }
/** /**
* 批量充值记录列表 * 批量充值记录列表
* @param params 查询参数 * @param params 查询参数
*/ */
static getBatchRechargeRecords(params?: any): Promise<PaginationResponse<BatchRechargeRecord>> { static getBatchRechargeRecords(
params?: ApiQueryParams
): Promise<PaginationResponse<BatchRechargeRecord>> {
return this.getPage<BatchRechargeRecord>('/api/cards/batch-recharge-records', params) return this.getPage<BatchRechargeRecord>('/api/cards/batch-recharge-records', params)
} }
@@ -258,7 +276,7 @@ export class CardService extends BaseService {
* @param params 查询参数 * @param params 查询参数
*/ */
static getCardChangeApplications( static getCardChangeApplications(
params?: any params?: ApiQueryParams
): Promise<PaginationResponse<CardChangeApplication>> { ): Promise<PaginationResponse<CardChangeApplication>> {
return this.getPage<CardChangeApplication>('/api/card-change-applications', params) return this.getPage<CardChangeApplication>('/api/card-change-applications', params)
} }
@@ -284,8 +302,10 @@ export class CardService extends BaseService {
* 获取换卡通知记录 * 获取换卡通知记录
* @param params 查询参数 * @param params 查询参数
*/ */
static getCardChangeNotices(params?: any): Promise<PaginationResponse<any>> { static getCardChangeNotices(
return this.getPage('/api/card-change-notices', params) params?: ApiQueryParams
): Promise<PaginationResponse<CardChangeNotice>> {
return this.getPage<CardChangeNotice>('/api/card-change-notices', params)
} }
// ========== ICCID批量导入相关 ========== // ========== ICCID批量导入相关 ==========
@@ -302,8 +322,10 @@ export class CardService extends BaseService {
* 获取导入任务列表 * 获取导入任务列表
* @param params 查询参数 * @param params 查询参数
*/ */
static getIotCardImportTasks(params?: any): Promise<PaginationResponse<IotCardImportTask>> { static getIotCardImportTasks(
return this.getPage('/api/admin/iot-cards/import-tasks', params) params?: IotCardImportTaskQueryParams
): Promise<PaginationResponse<IotCardImportTask>> {
return this.getPage<IotCardImportTask>('/api/admin/iot-cards/import-tasks', params)
} }
/** /**
@@ -330,16 +352,26 @@ export class CardService extends BaseService {
* 批量分配单卡 * 批量分配单卡
* @param data 分配参数 * @param data 分配参数
*/ */
static allocateStandaloneCards(data: any): Promise<BaseResponse<any>> { static allocateStandaloneCards(
return this.post('/api/admin/iot-cards/standalone/allocate', data) data: Partial<AllocateStandaloneCardsRequest>
): Promise<BaseResponse<AllocateStandaloneCardsResponse>> {
return this.post<BaseResponse<AllocateStandaloneCardsResponse>>(
'/api/admin/iot-cards/standalone/allocate',
data
)
} }
/** /**
* 批量回收单卡 * 批量回收单卡
* @param data 回收参数 * @param data 回收参数
*/ */
static recallStandaloneCards(data: any): Promise<BaseResponse<any>> { static recallStandaloneCards(
return this.post('/api/admin/iot-cards/standalone/recall', data) data: Partial<RecallStandaloneCardsRequest>
): Promise<BaseResponse<AllocateStandaloneCardsResponse>> {
return this.post<BaseResponse<AllocateStandaloneCardsResponse>>(
'/api/admin/iot-cards/standalone/recall',
data
)
} }
// ========== 资产分配记录相关 ========== // ========== 资产分配记录相关 ==========
@@ -348,16 +380,20 @@ export class CardService extends BaseService {
* 获取资产分配记录列表 * 获取资产分配记录列表
* @param params 查询参数 * @param params 查询参数
*/ */
static getAssetAllocationRecords(params?: any): Promise<PaginationResponse<any>> { static getAssetAllocationRecords(
return this.getPage('/api/admin/asset-allocation-records', params) params?: AssetAllocationRecordQueryParams
): Promise<PaginationResponse<AssetAllocationRecord>> {
return this.getPage<AssetAllocationRecord>('/api/admin/asset-allocation-records', params)
} }
/** /**
* 获取资产分配记录详情 * 获取资产分配记录详情
* @param id 记录ID * @param id 记录ID
*/ */
static getAssetAllocationRecordDetail(id: number): Promise<BaseResponse<any>> { static getAssetAllocationRecordDetail(
return this.getOne(`/api/admin/asset-allocation-records/${id}`) id: number
): Promise<BaseResponse<AssetAllocationRecordDetail>> {
return this.getOne<AssetAllocationRecordDetail>(`/api/admin/asset-allocation-records/${id}`)
} }
// ========== 批量设置卡的套餐系列绑定相关 ========== // ========== 批量设置卡的套餐系列绑定相关 ==========

View File

@@ -13,8 +13,6 @@ import type {
WithdrawalSettingItem, WithdrawalSettingItem,
CreateWithdrawalSettingParams, CreateWithdrawalSettingParams,
CommissionRecordQueryParams, CommissionRecordQueryParams,
MyCommissionRecordPageResult,
MyCommissionSummary,
SubmitWithdrawalParams, SubmitWithdrawalParams,
ShopCommissionRecordPageResult, ShopCommissionRecordPageResult,
ShopFundSummaryQueryParams, ShopFundSummaryQueryParams,

View File

@@ -1,74 +0,0 @@
/**
* 客户账号管理 API (企业账号)
*
* @deprecated 此 API 已废弃,请使用统一的 AccountService 代替
* @see AccountService - 统一账号管理接口 (/api/admin/accounts)
*
* 迁移说明:
* - 所有账号类型统一使用 /api/admin/accounts 接口
* - 通过 user_type 参数区分账号类型 (2=平台, 3=代理, 4=企业)
* - customer-accounts 已改名为 enterprise企业账号
* - 详见docs/迁移指南.md
*/
import { BaseService } from '../BaseService'
import type { BaseResponse } from '@/types/api'
import type {
CustomerAccountItem,
CustomerAccountPageResult,
CustomerAccountQueryParams,
CreateCustomerAccountParams,
UpdateCustomerAccountParams,
UpdateCustomerAccountPasswordParams,
UpdateCustomerAccountStatusParams
} from '@/types/api/customerAccount'
export class CustomerAccountService extends BaseService {
/**
* 查询客户账号列表
*/
static getCustomerAccounts(
params?: CustomerAccountQueryParams
): Promise<BaseResponse<CustomerAccountPageResult>> {
return this.get<BaseResponse<CustomerAccountPageResult>>('/api/admin/customer-accounts', params)
}
/**
* 新增代理商账号
*/
static createCustomerAccount(
data: CreateCustomerAccountParams
): Promise<BaseResponse<CustomerAccountItem>> {
return this.post<BaseResponse<CustomerAccountItem>>('/api/admin/customer-accounts', data)
}
/**
* 编辑账号
*/
static updateCustomerAccount(
id: number,
data: UpdateCustomerAccountParams
): Promise<BaseResponse<CustomerAccountItem>> {
return this.put<BaseResponse<CustomerAccountItem>>(`/api/admin/customer-accounts/${id}`, data)
}
/**
* 修改账号密码
*/
static updateCustomerAccountPassword(
id: number,
data: UpdateCustomerAccountPasswordParams
): Promise<BaseResponse> {
return this.put<BaseResponse>(`/api/admin/customer-accounts/${id}/password`, data)
}
/**
* 修改账号状态
*/
static updateCustomerAccountStatus(
id: number,
data: UpdateCustomerAccountStatusParams
): Promise<BaseResponse> {
return this.put<BaseResponse>(`/api/admin/customer-accounts/${id}/status`, data)
}
}

View File

@@ -241,7 +241,7 @@ export class DeviceService extends BaseService {
/** /**
* 设置切卡模式 * 设置切卡模式
* @param identifier 设备标识(虚拟号 / IMEI / SN * @param identifier 设备标识(虚拟号 / IMEI / SN
* @param switchMode 切卡模式0=自动切卡1=手动切卡) * @param data
*/ */
static setSwitchMode( static setSwitchMode(
identifier: string, identifier: string,

View File

@@ -3,7 +3,7 @@
*/ */
import { BaseService } from '../BaseService' import { BaseService } from '../BaseService'
import type { BaseResponse, PaginationResponse } from '@/types/api' import type { BaseResponse } from '@/types/api'
// 换货单查询参数 // 换货单查询参数
export interface ExchangeQueryParams { export interface ExchangeQueryParams {

View File

@@ -2,21 +2,15 @@
* API 服务模块统一导出 * API 服务模块统一导出
*/ */
// 旧模块(待重构)
export * from './article'
// 新模块 // 新模块
export { AuthService } from './auth' export { AuthService } from './auth'
export { RoleService } from './role' export { RoleService } from './role'
export { PermissionService } from './permission' export { PermissionService } from './permission'
export { AccountService } from './account' export { AccountService } from './account'
export { PlatformAccountService } from './platformAccount'
export { ShopAccountService } from './shopAccount'
export { ShopService } from './shop' export { ShopService } from './shop'
export { CardService } from './card' export { CardService } from './card'
export { CommissionService } from './commission' export { CommissionService } from './commission'
export { EnterpriseService } from './enterprise' export { EnterpriseService } from './enterprise'
export { CustomerAccountService } from './customerAccount'
export { StorageService } from './storage' export { StorageService } from './storage'
export { AuthorizationService } from './authorization' export { AuthorizationService } from './authorization'
export { DeviceService } from './device' export { DeviceService } from './device'

View File

@@ -10,10 +10,8 @@ import type {
UpdatePackageRequest, UpdatePackageRequest,
UpdatePackageStatusRequest, UpdatePackageStatusRequest,
UpdatePackageShelfStatusRequest, UpdatePackageShelfStatusRequest,
SeriesSelectOption,
BaseResponse, BaseResponse,
PaginationResponse, PaginationResponse,
ListResponse
} from '@/types/api' } from '@/types/api'
export class PackageManageService extends BaseService { export class PackageManageService extends BaseService {

View File

@@ -6,23 +6,12 @@ import { BaseService } from '../BaseService'
import type { import type {
Permission, Permission,
PermissionTreeNode, PermissionTreeNode,
PermissionQueryParams,
CreatePermissionParams, CreatePermissionParams,
UpdatePermissionParams, UpdatePermissionParams,
BaseResponse, BaseResponse
PaginationResponse
} from '@/types/api' } from '@/types/api'
export class PermissionService extends BaseService { export class PermissionService extends BaseService {
/**
* 获取权限列表(分页)
* GET /api/admin/permissions
* @param params 查询参数
*/
static getPermissions(params?: PermissionQueryParams): Promise<PaginationResponse<Permission>> {
return this.getPage<Permission>('/api/admin/permissions', params)
}
/** /**
* 获取权限树 * 获取权限树
* GET /api/admin/permissions/tree * GET /api/admin/permissions/tree

View File

@@ -1,140 +0,0 @@
/**
* 平台账号相关 API - 匹配后端实际接口
*
* @deprecated 此 API 已废弃,请使用统一的 AccountService 代替
* @see AccountService - 统一账号管理接口 (/api/admin/accounts)
*
* 迁移说明:
* - 所有账号类型统一使用 /api/admin/accounts 接口
* - 通过 user_type 参数区分账号类型 (2=平台, 3=代理, 4=企业)
* - 详见docs/迁移指南.md
*/
import { BaseService } from '../BaseService'
import type {
PlatformAccountResponse,
PlatformAccountQueryParams,
CreatePlatformAccountParams,
UpdatePlatformAccountParams,
ChangePlatformAccountPasswordParams,
AssignRolesParams,
UpdateAccountStatusParams,
PlatformAccountRoleResponse,
BaseResponse,
PaginationResponse
} from '@/types/api'
export class PlatformAccountService extends BaseService {
/**
* 1. 获取平台账号列表
* GET /api/admin/platform-accounts
* @param params 查询参数
*/
static getPlatformAccounts(
params?: PlatformAccountQueryParams
): Promise<PaginationResponse<PlatformAccountResponse>> {
return this.getPage<PlatformAccountResponse>('/api/admin/platform-accounts', params)
}
/**
* 2. 新增平台账号
* POST /api/admin/platform-accounts
* @param data 账号数据
*/
static createPlatformAccount(
data: CreatePlatformAccountParams
): Promise<BaseResponse<PlatformAccountResponse>> {
return this.post<BaseResponse<PlatformAccountResponse>>('/api/admin/platform-accounts', data)
}
/**
* 3. 移除角色
* DELETE /api/admin/platform-accounts/{account_id}/roles/{role_id}
* @param accountId 账号ID
* @param roleId 角色ID
*/
static removeRoleFromPlatformAccount(accountId: number, roleId: number): Promise<BaseResponse> {
return this.delete<BaseResponse>(`/api/admin/platform-accounts/${accountId}/roles/${roleId}`)
}
/**
* 4. 删除平台账号
* DELETE /api/admin/platform-accounts/{id}
* @param id 账号ID
*/
static deletePlatformAccount(id: number): Promise<BaseResponse> {
return this.remove(`/api/admin/platform-accounts/${id}`)
}
/**
* 5. 获取平台账号详情
* GET /api/admin/platform-accounts/{id}
* @param id 账号ID
*/
static getPlatformAccountDetail(id: number): Promise<BaseResponse<PlatformAccountResponse>> {
return this.getOne<PlatformAccountResponse>(`/api/admin/platform-accounts/${id}`)
}
/**
* 6. 编辑平台账号
* PUT /api/admin/platform-accounts/{id}
* @param id 账号ID
* @param data 更新数据
*/
static updatePlatformAccount(
id: number,
data: UpdatePlatformAccountParams
): Promise<BaseResponse<PlatformAccountResponse>> {
return this.put<BaseResponse<PlatformAccountResponse>>(
`/api/admin/platform-accounts/${id}`,
data
)
}
/**
* 7. 修改密码
* PUT /api/admin/platform-accounts/{id}/password
* @param id 账号ID
* @param data 新密码
*/
static changePlatformAccountPassword(
id: number,
data: ChangePlatformAccountPasswordParams
): Promise<BaseResponse> {
return this.put<BaseResponse>(`/api/admin/platform-accounts/${id}/password`, data)
}
/**
* 8. 获取账号角色
* GET /api/admin/platform-accounts/{id}/roles
* @param id 账号ID
*/
static getPlatformAccountRoles(id: number): Promise<BaseResponse<PlatformAccountRoleResponse[]>> {
return this.get<BaseResponse<PlatformAccountRoleResponse[]>>(
`/api/admin/platform-accounts/${id}/roles`
)
}
/**
* 9. 分配角色
* POST /api/admin/platform-accounts/{id}/roles
* @param id 账号ID
* @param data 角色ID列表
*/
static assignRolesToPlatformAccount(id: number, data: AssignRolesParams): Promise<BaseResponse> {
return this.post<BaseResponse>(`/api/admin/platform-accounts/${id}/roles`, data)
}
/**
* 10. 启用/禁用账号
* PUT /api/admin/platform-accounts/{id}/status
* @param id 账号ID
* @param data 状态
*/
static updatePlatformAccountStatus(
id: number,
data: UpdateAccountStatusParams
): Promise<BaseResponse> {
return this.put<BaseResponse>(`/api/admin/platform-accounts/${id}/status`, data)
}
}

View File

@@ -1,86 +0,0 @@
/**
* 代理账号相关 API - 匹配后端实际接口
*
* @deprecated 此 API 已废弃,请使用统一的 AccountService 代替
* @see AccountService - 统一账号管理接口 (/api/admin/accounts)
*
* 迁移说明:
* - 所有账号类型统一使用 /api/admin/accounts 接口
* - 通过 user_type 参数区分账号类型 (2=平台, 3=代理, 4=企业)
* - 详见docs/迁移指南.md
*/
import { BaseService } from '../BaseService'
import type {
ShopAccountResponse,
ShopAccountQueryParams,
CreateShopAccountParams,
UpdateShopAccountParams,
UpdateShopAccountPasswordParams,
UpdateShopAccountStatusParams,
BaseResponse,
PaginationResponse
} from '@/types/api'
export class ShopAccountService extends BaseService {
/**
* 获取代理账号列表
* GET /api/admin/shop-accounts
* @param params 查询参数
*/
static getShopAccounts(
params?: ShopAccountQueryParams
): Promise<PaginationResponse<ShopAccountResponse>> {
return this.getPage<ShopAccountResponse>('/api/admin/shop-accounts', params)
}
/**
* 创建代理账号
* POST /api/admin/shop-accounts
* @param data 代理账号数据
*/
static createShopAccount(
data: CreateShopAccountParams
): Promise<BaseResponse<ShopAccountResponse>> {
return this.post<BaseResponse<ShopAccountResponse>>('/api/admin/shop-accounts', data)
}
/**
* 更新代理账号
* PUT /api/admin/shop-accounts/{id}
* @param id 账号ID
* @param data 更新数据
*/
static updateShopAccount(
id: number,
data: UpdateShopAccountParams
): Promise<BaseResponse<ShopAccountResponse>> {
return this.put<BaseResponse<ShopAccountResponse>>(`/api/admin/shop-accounts/${id}`, data)
}
/**
* 重置代理账号密码
* PUT /api/admin/shop-accounts/{id}/password
* @param id 账号ID
* @param data 密码数据
*/
static updateShopAccountPassword(
id: number,
data: UpdateShopAccountPasswordParams
): Promise<BaseResponse> {
return this.put<BaseResponse>(`/api/admin/shop-accounts/${id}/password`, data)
}
/**
* 启用/禁用代理账号
* PUT /api/admin/shop-accounts/{id}/status
* @param id 账号ID
* @param data 状态数据
*/
static updateShopAccountStatus(
id: number,
data: UpdateShopAccountStatusParams
): Promise<BaseResponse> {
return this.put<BaseResponse>(`/api/admin/shop-accounts/${id}/status`, data)
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 954 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 726 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 944 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 810 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -362,8 +362,8 @@
color: var(--el-text-color-regular); color: var(--el-text-color-regular);
em { em {
color: var(--el-color-primary);
font-style: normal; font-style: normal;
color: var(--el-color-primary);
} }
} }
} }

View File

@@ -1,140 +0,0 @@
<template>
<div class="card-banner art-custom-card">
<div class="banner-content">
<div class="banner-icon">
<img :src="props.icon" :alt="props.title" />
</div>
<div class="banner-text">
<p class="banner-title">{{ props.title }}</p>
<p class="banner-description">{{ props.description }}</p>
</div>
<div class="banner-buttons">
<div
v-if="showCancel"
class="banner-button cancel-button"
:style="{ backgroundColor: cancelButtonColor, color: cancelButtonTextColor }"
@click="handleCancel"
>
{{ cancelButtonText }}
</div>
<div
class="banner-button"
:style="{ backgroundColor: buttonColor, color: buttonTextColor }"
@click="handleClick"
>
{{ buttonText }}
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import defaultIcon from '@imgs/3d/icon1.webp'
interface CardBannerProps {
icon?: string
title: string
description: string
buttonText?: string
buttonColor?: string
buttonTextColor?: string
showCancel?: boolean
cancelButtonText?: string
cancelButtonColor?: string
cancelButtonTextColor?: string
}
const props = withDefaults(defineProps<CardBannerProps>(), {
icon: defaultIcon,
title: '',
description: '',
buttonText: '重试',
buttonColor: 'var(--main-color)',
buttonTextColor: '#fff',
showCancel: false,
cancelButtonText: '取消',
cancelButtonColor: '#f5f5f5',
cancelButtonTextColor: '#666'
})
const emit = defineEmits<{
(e: 'click'): void
(e: 'cancel'): void
}>()
const handleClick = () => {
emit('click')
}
const handleCancel = () => {
emit('cancel')
}
</script>
<style lang="scss" scoped>
.card-banner {
padding: 3rem 0 4rem;
background-color: var(--art-main-bg-color);
border-radius: calc(var(--custom-radius) + 2px) !important;
.banner-content {
display: flex;
flex-direction: column;
gap: 16px;
align-items: center;
text-align: center;
}
.banner-icon {
width: 180px;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.banner-text {
.banner-title {
margin-bottom: 8px;
font-size: 18px;
font-weight: 600;
color: var(--art-text-gray-800);
}
.banner-description {
margin: 0;
font-size: 14px;
color: var(--art-text-gray-600);
}
}
.banner-buttons {
display: flex;
gap: 12px;
align-items: center;
}
.banner-button {
display: inline-block;
height: var(--el-component-custom-height);
padding: 0 12px;
font-size: 14px;
line-height: var(--el-component-custom-height);
cursor: pointer;
user-select: none;
border-radius: 6px;
transition: opacity 0.3s;
&:hover {
opacity: 0.9;
}
&.cancel-button {
border: 1px solid #dcdfe6;
}
}
}
</style>

View File

@@ -20,7 +20,9 @@
<div class="chat-messages" ref="messageContainer"> <div class="chat-messages" ref="messageContainer">
<template v-for="(message, index) in messages" :key="index"> <template v-for="(message, index) in messages" :key="index">
<div :class="['message-item', message.isMe ? 'message-right' : 'message-left']"> <div :class="['message-item', message.isMe ? 'message-right' : 'message-left']">
<el-avatar :size="32" :src="message.avatar" class="message-avatar" /> <el-avatar :size="32" class="message-avatar">
{{ message.sender.slice(0, 1) }}
</el-avatar>
<div class="message-content"> <div class="message-content">
<div class="message-info"> <div class="message-info">
<span class="sender-name">{{ message.sender }}</span> <span class="sender-name">{{ message.sender }}</span>
@@ -67,8 +69,6 @@
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { Picture, Paperclip } from '@element-plus/icons-vue' import { Picture, Paperclip } from '@element-plus/icons-vue'
import { mittBus } from '@/utils/sys' import { mittBus } from '@/utils/sys'
import meAvatar from '@/assets/img/avatar/avatar5.webp'
import aiAvatar from '@/assets/img/avatar/avatar10.webp'
const { width } = useWindowSize() const { width } = useWindowSize()
const isMobile = computed(() => width.value < 500) const isMobile = computed(() => width.value < 500)
@@ -86,79 +86,68 @@
sender: 'Art Bot', sender: 'Art Bot',
content: '你好我是你的AI助手有什么我可以帮你的吗', content: '你好我是你的AI助手有什么我可以帮你的吗',
time: '10:00', time: '10:00',
isMe: false, isMe: false
avatar: aiAvatar
}, },
{ {
id: 2, id: 2,
sender: 'Ricky', sender: 'Ricky',
content: '我想了解一下系统的使用方法。', content: '我想了解一下系统的使用方法。',
time: '10:01', time: '10:01',
isMe: true, isMe: true
avatar: meAvatar
}, },
{ {
id: 3, id: 3,
sender: 'Art Bot', sender: 'Art Bot',
content: '好的,我来为您介绍系统的主要功能。首先,您可以通过左侧菜单访问不同的功能模块...', content: '好的,我来为您介绍系统的主要功能。首先,您可以通过左侧菜单访问不同的功能模块...',
time: '10:02', time: '10:02',
isMe: false, isMe: false
avatar: aiAvatar
}, },
{ {
id: 4, id: 4,
sender: 'Ricky', sender: 'Ricky',
content: '听起来很不错,能具体讲讲数据分析部分吗?', content: '听起来很不错,能具体讲讲数据分析部分吗?',
time: '10:05', time: '10:05',
isMe: true, isMe: true
avatar: meAvatar
}, },
{ {
id: 5, id: 5,
sender: 'Art Bot', sender: 'Art Bot',
content: '当然可以。数据分析模块可以帮助您实时监控关键指标,并生成详细的报表...', content: '当然可以。数据分析模块可以帮助您实时监控关键指标,并生成详细的报表...',
time: '10:06', time: '10:06',
isMe: false, isMe: false
avatar: aiAvatar
}, },
{ {
id: 6, id: 6,
sender: 'Ricky', sender: 'Ricky',
content: '太好了,那我如何开始使用呢?', content: '太好了,那我如何开始使用呢?',
time: '10:08', time: '10:08',
isMe: true, isMe: true
avatar: meAvatar
}, },
{ {
id: 7, id: 7,
sender: 'Art Bot', sender: 'Art Bot',
content: '您可以先创建一个项目,然后在项目中添加相关的数据源,系统会自动进行分析。', content: '您可以先创建一个项目,然后在项目中添加相关的数据源,系统会自动进行分析。',
time: '10:09', time: '10:09',
isMe: false, isMe: false
avatar: aiAvatar
}, },
{ {
id: 8, id: 8,
sender: 'Ricky', sender: 'Ricky',
content: '明白了,谢谢你的帮助!', content: '明白了,谢谢你的帮助!',
time: '10:10', time: '10:10',
isMe: true, isMe: true
avatar: meAvatar
}, },
{ {
id: 9, id: 9,
sender: 'Art Bot', sender: 'Art Bot',
content: '不客气,有任何问题随时联系我。', content: '不客气,有任何问题随时联系我。',
time: '10:11', time: '10:11',
isMe: false, isMe: false
avatar: aiAvatar
} }
]) ])
const messageId = ref(10) // 用于生成唯一的消息ID const messageId = ref(10) // 用于生成唯一的消息ID
const userAvatar = ref(meAvatar) // 使用导入的头像
// 发送消息 // 发送消息
const sendMessage = () => { const sendMessage = () => {
const text = messageText.value.trim() const text = messageText.value.trim()
@@ -169,8 +158,7 @@
sender: 'Ricky', sender: 'Ricky',
content: text, content: text,
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
isMe: true, isMe: true
avatar: userAvatar.value
}) })
messageText.value = '' messageText.value = ''

View File

@@ -78,13 +78,6 @@
const applications = computed<Application[]>(() => { const applications = computed<Application[]>(() => {
const apps: Application[] = [ const apps: Application[] = [
{
name: '聊天',
description: '即时通讯功能',
icon: '&#xe70a;',
iconColor: '#13DEB9',
path: RoutesAlias.Chat
},
{ {
name: '官方文档', name: '官方文档',
description: '使用指南与开发文档', description: '使用指南与开发文档',

View File

@@ -22,7 +22,7 @@
CIRCLE: { SIZE: 12 }, // 圆形直径 CIRCLE: { SIZE: 12 }, // 圆形直径
TRIANGLE: { SIZE: 10 }, // 三角形边长 TRIANGLE: { SIZE: 10 }, // 三角形边长
OVAL: { WIDTH: 24, HEIGHT: 12 }, // 椭圆宽高 OVAL: { WIDTH: 24, HEIGHT: 12 }, // 椭圆宽高
IMAGE: { WIDTH: 30, HEIGHT: 30 } // 图片尺寸 IMAGE: { WIDTH: 30, HEIGHT: 30 } // 保留尺寸配置,兼容历史粒子类型
}, },
// 旋转相关参数 // 旋转相关参数
ROTATION: { ROTATION: {
@@ -61,32 +61,6 @@
] ]
} }
// 图片缓存
const imageCache: { [url: string]: HTMLImageElement } = {}
// 预加载图片函数
const preloadImage = (url: string): Promise<HTMLImageElement> => {
return new Promise((resolve, reject) => {
if (imageCache[url]) {
if (imageCache[url].complete) {
resolve(imageCache[url])
} else {
imageCache[url].onload = () => resolve(imageCache[url])
imageCache[url].onerror = reject
}
} else {
const img = new Image()
img.crossOrigin = 'anonymous' // 处理CORS
img.src = url
img.onload = () => {
imageCache[url] = img
resolve(img)
}
img.onerror = reject
}
})
}
interface Firework { interface Firework {
x: number x: number
y: number y: number
@@ -99,7 +73,6 @@
shape: string shape: string
active: boolean active: boolean
rotationSpeed: { x: number; y: number; z: number } rotationSpeed: { x: number; y: number; z: number }
imageUrl?: string
opacity: number // 新增透明度属性 opacity: number // 新增透明度属性
} }
@@ -139,14 +112,12 @@
} }
// 创建烟花 // 创建烟花
const createFirework = (imageUrl?: string) => { const createFirework = () => {
// 异步创建粒子,避免阻塞主线程 // 异步创建粒子,避免阻塞主线程
setTimeout(() => { setTimeout(() => {
const startX = Math.random() * window.innerWidth const startX = Math.random() * window.innerWidth
const startY = window.innerHeight const startY = window.innerHeight
const availableShapes = imageUrl && imageCache[imageUrl] ? ['image'] : CONFIG.SHAPES
for (let i = 0; i < CONFIG.PARTICLES_PER_BURST; i++) { for (let i = 0; i < CONFIG.PARTICLES_PER_BURST; i++) {
const particle = getParticleFromPool() const particle = getParticleFromPool()
if (!particle) continue if (!particle) continue
@@ -167,8 +138,7 @@
rotationX: Math.random() * 360 - 180, rotationX: Math.random() * 360 - 180,
rotationY: Math.random() * 360 - 180, rotationY: Math.random() * 360 - 180,
scale: 0.8 + Math.random() * 0.4, scale: 0.8 + Math.random() * 0.4,
shape: availableShapes[Math.floor(Math.random() * availableShapes.length)], shape: CONFIG.SHAPES[Math.floor(Math.random() * CONFIG.SHAPES.length)],
imageUrl: imageUrl && imageCache[imageUrl] ? imageUrl : undefined,
rotationSpeed: { rotationSpeed: {
x: x:
(Math.random() * CONFIG.ROTATION.RANDOM_SPEED + CONFIG.ROTATION.BASE_SPEED) * (Math.random() * CONFIG.ROTATION.RANDOM_SPEED + CONFIG.ROTATION.BASE_SPEED) *
@@ -289,20 +259,6 @@
ctx.value.closePath() ctx.value.closePath()
ctx.value.fill() ctx.value.fill()
break break
case 'image':
if (firework.imageUrl) {
const img = imageCache[firework.imageUrl]
if (img && img.complete) {
ctx.value.drawImage(
img,
-CONFIG.SIZES.IMAGE.WIDTH / 2,
-CONFIG.SIZES.IMAGE.HEIGHT / 2,
CONFIG.SIZES.IMAGE.WIDTH,
CONFIG.SIZES.IMAGE.HEIGHT
)
}
}
break
default: default:
break break
} }
@@ -351,43 +307,20 @@
} }
} }
import bp from '@imgs/ceremony/hb.png' onMounted(() => {
import sd from '@imgs/ceremony/sd.png'
import yd from '@imgs/ceremony/yd.png'
// 预加载所有需要的图片
const preloadAllImages = async () => {
const imageUrls = [bp, sd, yd]
try {
await Promise.all(imageUrls.map((url) => preloadImage(url)))
} catch (error) {
console.error('Image preloading failed', error)
}
}
onMounted(async () => {
if (canvas.value) { if (canvas.value) {
ctx.value = canvas.value.getContext('2d') ctx.value = canvas.value.getContext('2d')
resizeCanvas() resizeCanvas()
} }
initParticlePool() initParticlePool()
// 预加载所有图片
await preloadAllImages()
animate() animate()
useEventListener(window, 'keydown', handleKeyPress) useEventListener(window, 'keydown', handleKeyPress)
useEventListener(window, 'resize', resizeCanvas) useEventListener(window, 'resize', resizeCanvas)
// 监听触发烟花的事件 // 监听触发烟花的事件
mittBus.on('triggerFireworks', ((event: unknown) => { mittBus.on('triggerFireworks', (() => {
const imageUrl = event as string | undefined createFirework()
if (imageUrl && imageCache[imageUrl]?.complete) {
createFirework(imageUrl)
} else {
createFirework()
}
}) as Handler<unknown>) }) as Handler<unknown>)
}) })

View File

@@ -48,7 +48,7 @@
<ul class="user-list" v-show="barActiveIndex === 1"> <ul class="user-list" v-show="barActiveIndex === 1">
<li v-for="(item, index) in msgList" :key="index"> <li v-for="(item, index) in msgList" :key="index">
<div class="avatar"> <div class="avatar">
<img :src="item.avatar" /> <span>{{ item.title.slice(0, 1) }}</span>
</div> </div>
<div class="text"> <div class="text">
<h4>{{ item.title }}</h4> <h4>{{ item.title }}</h4>
@@ -89,12 +89,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import avatar1 from '@/assets/img/avatar/avatar1.webp'
import avatar2 from '@/assets/img/avatar/avatar2.webp'
import avatar3 from '@/assets/img/avatar/avatar3.webp'
import avatar4 from '@/assets/img/avatar/avatar4.webp'
import avatar5 from '@/assets/img/avatar/avatar5.webp'
import avatar6 from '@/assets/img/avatar/avatar6.webp'
import AppConfig from '@/config' import AppConfig from '@/config'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
@@ -169,33 +163,27 @@
const msgList: any = [ const msgList: any = [
{ {
title: '池不胖 关注了你', title: '池不胖 关注了你',
time: '2021-2-26 23:50', time: '2021-2-26 23:50'
avatar: avatar1
}, },
{ {
title: '唐不苦 关注了你', title: '唐不苦 关注了你',
time: '2021-2-21 8:05', time: '2021-2-21 8:05'
avatar: avatar2
}, },
{ {
title: '中小鱼 关注了你', title: '中小鱼 关注了你',
time: '2020-1-17 21:12', time: '2020-1-17 21:12'
avatar: avatar3
}, },
{ {
title: '何小荷 关注了你', title: '何小荷 关注了你',
time: '2021-01-14 0:20', time: '2021-01-14 0:20'
avatar: avatar4
}, },
{ {
title: '誶誶淰 关注了你', title: '誶誶淰 关注了你',
time: '2020-12-20 0:15', time: '2020-12-20 0:15'
avatar: avatar5
}, },
{ {
title: '冷月呆呆 关注了你', title: '冷月呆呆 关注了你',
time: '2020-12-17 22:06', time: '2020-12-17 22:06'
avatar: avatar6
} }
] ]

View File

@@ -17,7 +17,7 @@
</ElRadioGroup> </ElRadioGroup>
</ElFormItem> </ElFormItem>
<ElFormItem> <ElFormItem>
<div style="font-size: 12px; color: #909399; line-height: 1.6"> <div style="font-size: 12px; line-height: 1.6; color: #909399">
<p>说明</p> <p>说明</p>
<p> <strong>无需实名</strong>用户使用设备无需进行实名认证</p> <p> <strong>无需实名</strong>用户使用设备无需进行实名认证</p>
<p> <strong>先实名后充值/购买</strong>用户必须先完成实名认证才能进行充值或购买套餐</p> <p> <strong>先实名后充值/购买</strong>用户必须先完成实名认证才能进行充值或购买套餐</p>

View File

@@ -39,8 +39,7 @@ export function useCeremony() {
const { start: startFireworks } = useTimeoutFn(() => { const { start: startFireworks } = useTimeoutFn(() => {
const { pause } = useIntervalFn(() => { const { pause } = useIntervalFn(() => {
// console.log(currentFestivalData.value?.image) mittBus.emit('triggerFireworks')
mittBus.emit('triggerFireworks', currentFestivalData.value?.image)
triggers++ triggers++
if (triggers >= FESTIVAL_CONFIG.MAX_TRIGGERS) { if (triggers >= FESTIVAL_CONFIG.MAX_TRIGGERS) {

View File

@@ -4,20 +4,16 @@
*/ */
// 图片需要在 components/Ceremony/Fireworks 文件预先定义 // 图片需要在 components/Ceremony/Fireworks 文件预先定义
import { FestivalConfig } from '@/types/config' import { FestivalConfig } from '@/types/config'
import sd from '@imgs/ceremony/sd.png'
import yd from '@imgs/ceremony/yd.png'
export const festivalConfigList: FestivalConfig[] = [ export const festivalConfigList: FestivalConfig[] = [
{ {
date: '2025-01-01', date: '2025-01-01',
name: '元旦', name: '元旦',
image: yd,
scrollText: '新年快乐Art Design Pro 祝您在2025年万事如意事业腾飞阖家幸福好运连连' scrollText: '新年快乐Art Design Pro 祝您在2025年万事如意事业腾飞阖家幸福好运连连'
}, },
{ {
date: '2024-12-25', date: '2024-12-25',
name: '圣诞节', name: '圣诞节',
image: sd,
scrollText: 'Merry ChristmasArt Design Pro 祝您圣诞快乐,愿节日的欢乐与祝福如雪花般纷至沓来!' scrollText: 'Merry ChristmasArt Design Pro 祝您圣诞快乐,愿节日的欢乐与祝福如雪花般纷至沓来!'
} }
] ]

View File

@@ -326,13 +326,6 @@
"calendar": "Calendar", "calendar": "Calendar",
"pricing": "Pricing" "pricing": "Pricing"
}, },
"article": {
"title": "Article Management",
"articleList": "Article List",
"articleDetail": "Article Detail",
"comment": "Comment",
"articlePublish": "Article Publish"
},
"result": { "result": {
"title": "Result Page", "title": "Result Page",
"success": "Success", "success": "Success",

View File

@@ -1,194 +0,0 @@
import cover1 from '@imgs/cover/img1.webp'
import cover2 from '@imgs/cover/img2.webp'
import cover3 from '@imgs/cover/img3.webp'
import cover4 from '@imgs/cover/img4.webp'
import cover5 from '@imgs/cover/img5.webp'
import cover6 from '@imgs/cover/img6.webp'
import cover7 from '@imgs/cover/img7.webp'
import cover8 from '@imgs/cover/img8.webp'
import cover9 from '@imgs/cover/img9.webp'
import cover10 from '@imgs/cover/img10.webp'
import { ArticleType } from '@/api/modules'
export const ArticleList: ArticleType[] = [
{
id: 452,
blog_class: '42',
title: 'Node.js + Docker自动化部署',
count: 56,
create_time: '2024-08-26T00:00:00.000Z',
home_img: cover1,
brief:
'本章将介绍 Node.js 使用 Docker 、Webhook 自动化部署、蓝绿部署、项目到服务器。1、Mac os 安装 Docker 客户端 OrbStack我这里使用的是第三方客户端相比于官方的较轻量启动速度快OrbStack 是一种快速、轻便且简单的运行 Docker 容器和 Linux 的方法。使用我们的 Docker Desktop 替代方案以光速进行开发。下载地址: http',
type_name: 'Node.js',
html_content: ''
},
{
id: 451,
blog_class: '36',
title: 'HTTP 协议',
count: 109,
create_time: '2024-02-22T00:00:00.000Z',
home_img: cover2,
brief:
'概念HTTPhypertext transport protocol协议中文叫超文本传输协议是一种基于TCP/IP的应用层通信协议这个协议详细规定了 浏览器 和万维网 服务器 之间互相通信的规则。协议中主要规定了两个方面的内容客户端用来向服务器发送数据可以被称之为请求报文服务端向客户端返回数据可以被称之为响应报文报文可以简单理解为就是一堆字符串请求报文的组成请求行请求头空行请求体H',
type_name: '浏览器',
html_content: ''
},
{
id: 450,
blog_class: '40',
title: 'MongoDB 数据库基本操作',
count: 66,
create_time: '2023-11-30T00:00:00.000Z',
home_img: cover3,
brief:
'简介Mongodb 是什么MongoDB 是一个基于分布式文件存储的数据库,官方地址 https://www.mongodb.com/ 数据库是什么数据库DataBase是按照数据结构来组织、存储和管理数据的 应用程序数据库的作用数据库的主要作用就是 管理数据 ,对数据进行 增c、删d、改u、查r数据库管理数据的特点相比于纯文件管理数据数据库管理数据有如下特点1. 速度更快',
type_name: 'MongoDB',
html_content: ''
},
{
id: 449,
blog_class: '40',
title: 'Mac os 安装 MongoDB',
count: 59,
create_time: '2023-11-15T00:00:00.000Z',
home_img: cover4,
brief:
'下载MongoDB安装包官网下载地址https://www.mongodb.com/try/download/community?tck=docs_server安装MongoDB# 将压缩包解压到 /usr/local/目录下\nsudo tar -zxvf mongodb-macos-x86_64-5.0.24.tgz -C /usr/local\n\n# 重命名\nsudo mv mongodb-o',
type_name: 'MongoDB',
html_content: ''
},
{
id: 448,
blog_class: '42',
title: 'npm、yarn、nrm 常用命令',
count: 91,
create_time: '2023-11-07T00:00:00.000Z',
home_img: cover5,
brief:
'设置镜像源#1,淘宝镜像源\nnpm config set registry https://registry.npmmirror.com\nnpm config set registry https://registry.npm.taobao.org\n\n#2,腾讯云镜像源\nnpm config set registry http://mirrors.cloud.tencent.com/npm/\n\n#',
type_name: 'Node.js',
html_content: ''
},
{
id: 447,
blog_class: '42',
title: 'Node.js 包管理工具',
count: 53,
create_time: '2023-10-31T00:00:00.000Z',
home_img: cover6,
brief:
'介绍包是什么『包』英文单词是 package ,代表了一组特定功能的源码集合包管理工具管理『包』的应用软件,可以对「包」进行 下载安装 更新 删除 上传 等操作借助包管理工具,可以快速开发项目,提升开发效率包管理工具是一个通用的概念,很多编程语言都有包管理工具,所以 掌握好包管理工具非常重要常用的包管理工具下面列举了前端常用的包管理工具npmyarncnpmnpmnpm 全称 Node',
type_name: 'Node.js',
html_content: ''
},
{
id: 446,
blog_class: '42',
title: 'Node.js 模块化',
count: 40,
create_time: '2023-10-25T00:00:00.000Z',
home_img: cover7,
brief:
'介绍什么是模块化与模块 ?将一个复杂的程序文件依据一定规则(规范)拆分成多个文件的过程称之为其中拆分出的 每个文件就是一个模块 ,模块的内部数据是私有的,不过模块可以暴露内部数据以便其他模块使用什么是模块化项目 ?编码时是按照模块一个一个编码的, 整个项目就是一个模块化的项目模块化好处下面是模块化的一些好处1.防止命名冲突2.高复用性3.高维护性模块暴露数据模块初体验可以通过下面的操作步骤,快速',
type_name: 'Node.js',
html_content: ''
},
{
id: 445,
blog_class: '42',
title: 'Node.js学习笔记',
count: 198,
create_time: '2023-10-15T00:00:00.000Z',
home_img: cover8,
brief:
'fs 模块fs 全称为 file system ,称之为 文件系统 ,是 Node.js 中的 内置模块 ,可以对计算机中的磁盘进行操作。例如文件的创建、删除、修改移动,文件内容的写入、读取,以及文件夹的相关操作// 1 ------------------------------------------------------\n/**\n * 需求\n * 新建一个文件,写入内容\n */\n\n// 1',
type_name: 'Node.js',
html_content: ''
},
{
id: 444,
blog_class: '41',
title: '最好用的ChatGPT应用',
count: 78,
create_time: '2023-05-22T00:00:00.000Z',
home_img: cover9,
brief:
'目前为止最好用的ChatGPT网站支持6种AI模型和每日一次GPT4的使用机会有网页版和ios应用程序可以使用邮箱和手机号等方式注册中国大陆可放心使用需要科学上网下面就介绍一下如何使用吧。网站地址https://poe.com/进去之后可以选择邮箱或手机号等其他方式注册。除了网页版你还可以到AppStore搜索poe下载ios客户端',
type_name: 'GPT',
html_content: ''
},
{
id: 443,
blog_class: '35',
title: 'Nuxt 百度收录 robots 和 sitemap',
count: 109,
create_time: '2023-04-07T00:00:00.000Z',
home_img: cover10,
brief:
'前言robots 和 sitemap 文件前者的作用是减少百度蜘蛛在站内的无谓爬取后者增加百度蜘蛛在站内的有效爬取对百度收录和自己网站的SEO推广都十分重要。robots只有一个robot.txt这是一个文本文件主要利用Allow允许和DisAllow禁止两个命令这两个重要的是禁止禁止百度蜘蛛爬取一些无谓的文件和文件夹增加百度搜录速度。具体原理和写法网上去搜制作简单',
type_name: 'Nuxt',
html_content: ''
},
{
id: 442,
blog_class: '12',
title: 'Vue3+TS+Vite 项目搭建笔记(更新中)',
count: 516,
create_time: '2023-04-03T00:00:00.000Z',
home_img: cover4,
brief:
'介绍本章会教你在真实项目中如何搭建 VueRouter、Vuex、pinia、axios、主题切换等你会见证一个后台管理系统的详细搭建过程。效果图功能后台管理系统常用模块登录加密多标签页全局面包屑国际化异常处理Utils工具包可配置的菜单栏徽标亮色 / 暗色 侧边栏浅色主题 / 暗黑主题丰富的个性化配置可折叠侧边栏支持内嵌页面重载当前页面动态路由支持自动重载支持多级路由嵌套及菜单栏嵌套分离路',
type_name: 'Vue',
html_content: ''
},
{
id: 441,
blog_class: '9',
title: 'CSS 根据系统自动切换主题方案',
count: 184,
create_time: '2023-04-01T00:00:00.000Z',
home_img: cover8,
brief:
'原理是改变 css 变量 + window.matchMedia 来监听系统主题变从而实现点击改变主题和监听系统主题变化1、首先定义 css 全局变量创建 variables.scss 文件light color浅色模式定义浅色模式下 css 主题变量dark color深色模式定义深色模式下 css 主题变量// css全局变量\n:root {\n // 文字大小\n --art-fo',
type_name: 'CSS',
html_content: ''
},
{
id: 440,
blog_class: '36',
title: '浏览器-安全',
count: 116,
create_time: '2023-03-28T00:00:00.000Z',
home_img: cover2,
brief:
'通过这篇文章你可以了解到同源策略、跨站脚本攻击xss、跨域请求伪造CSRF以及安全沙箱相关知识以下是本文的思维导图手机端可能看不清获取高清 PDF请在微信公众号【小狮子前端】回复【浏览器安全】同源策略什么是同源策略如果两个 URL 的协议、域名和端口都相同,我们就称这两个 URL 同源。两个不同的源之间若想要相互访问资源或者操作 DOM那么会有一套基础的安全策略的制约我们把这',
type_name: '浏览器',
html_content: ''
},
{
id: 439,
blog_class: '12',
title: 'Vue-Router4',
count: 135,
create_time: '2023-02-08T00:00:00.000Z',
home_img: cover3,
brief:
"路由模式构建 router.tsimport { createRouter, createWebHistory, createWebHashHistory, createMemoryHistory, createRouterMatcher } from 'vue-router'\nimport Home from '../views/home/index.vue'\nimport Login from",
type_name: 'Vue',
html_content: ''
},
{
id: 438,
blog_class: '10',
title: 'Event Loop事件循环',
count: 161,
create_time: '2023-01-03T00:00:00.000Z',
home_img: cover1,
brief:
'js是单线程的一次只能执行一段代码。单线程会导致很多任务需要排队一个个去执行如果此时某个任务执行时间太长就会出现阻塞为了解决这个问题js引入了事件循环机制。为什么要区分宏任务和微任务js是单线程的但是分同步异步微任务和宏任务皆为异步任务它们都属于一个队列宏任务script整体代码、setTimeout、setInterval、I/O、UI、 renderingsetImme',
type_name: 'JavaScript',
html_content: ''
}
]

View File

@@ -1,273 +0,0 @@
import avatar1 from '@/assets/img/avatar/avatar1.webp'
import avatar2 from '@/assets/img/avatar/avatar2.webp'
import avatar3 from '@/assets/img/avatar/avatar3.webp'
import avatar4 from '@/assets/img/avatar/avatar4.webp'
import avatar5 from '@/assets/img/avatar/avatar5.webp'
import avatar6 from '@/assets/img/avatar/avatar6.webp'
import avatar7 from '@/assets/img/avatar/avatar7.webp'
import avatar8 from '@/assets/img/avatar/avatar8.webp'
import avatar9 from '@/assets/img/avatar/avatar9.webp'
import avatar10 from '@/assets/img/avatar/avatar10.webp'
export interface User {
id: number
username: string
gender: 1 | 0
mobile: string
email: string
dep: string
status: string
create_time: string
avatar: string
}
// 用户列表
export const ACCOUNT_TABLE_DATA: User[] = [
{
id: 1,
username: 'alexmorgan',
gender: 1,
mobile: '18670001591',
email: 'alexmorgan@company.com',
dep: '研发部',
status: '1',
create_time: '2020-09-09 10:01:10',
avatar: avatar1
},
{
id: 2,
username: 'sophiabaker',
gender: 1,
mobile: '17766664444',
email: 'sophiabaker@company.com',
dep: '电商部',
status: '1',
create_time: '2020-10-10 13:01:12',
avatar: avatar2
},
{
id: 3,
username: 'liampark',
gender: 1,
mobile: '18670001597',
email: 'liampark@company.com',
dep: '人事部',
status: '1',
create_time: '2020-11-14 12:01:45',
avatar: avatar3
},
{
id: 4,
username: 'oliviagrant',
gender: 0,
mobile: '18670001596',
email: 'oliviagrant@company.com',
dep: '产品部',
status: '1',
create_time: '2020-11-14 09:01:20',
avatar: avatar4
},
{
id: 5,
username: 'emmawilson',
gender: 0,
mobile: '18670001595',
email: 'emmawilson@company.com',
dep: '财务部',
status: '1',
create_time: '2020-11-13 11:01:05',
avatar: avatar5
},
{
id: 6,
username: 'noahevan',
gender: 1,
mobile: '18670001594',
email: 'noahevan@company.com',
dep: '运营部',
status: '1',
create_time: '2020-10-11 13:10:26',
avatar: avatar6
},
{
id: 7,
username: 'avamartin',
gender: 1,
mobile: '18123820191',
email: 'avamartin@company.com',
dep: '客服部',
status: '2',
create_time: '2020-05-14 12:05:10',
avatar: avatar7
},
{
id: 8,
username: 'jacoblee',
gender: 1,
mobile: '18670001592',
email: 'jacoblee@company.com',
dep: '总经办',
status: '3',
create_time: '2020-11-12 07:22:25',
avatar: avatar8
},
{
id: 9,
username: 'miaclark',
gender: 0,
mobile: '18670001581',
email: 'miaclark@company.com',
dep: '研发部',
status: '4',
create_time: '2020-06-12 05:04:20',
avatar: avatar9
},
{
id: 10,
username: 'ethanharris',
gender: 1,
mobile: '13755554444',
email: 'ethanharris@company.com',
dep: '研发部',
status: '1',
create_time: '2020-11-12 16:01:10',
avatar: avatar10
},
{
id: 11,
username: 'isabellamoore',
gender: 1,
mobile: '13766660000',
email: 'isabellamoore@company.com',
dep: '研发部',
status: '1',
create_time: '2020-11-14 12:01:20',
avatar: avatar6
},
{
id: 12,
username: 'masonwhite',
gender: 1,
mobile: '18670001502',
email: 'masonwhite@company.com',
dep: '研发部',
status: '1',
create_time: '2020-11-14 12:01:20',
avatar: avatar7
},
{
id: 13,
username: 'charlottehall',
gender: 1,
mobile: '13006644977',
email: 'charlottehall@company.com',
dep: '研发部',
status: '1',
create_time: '2020-11-14 12:01:20',
avatar: avatar8
},
{
id: 14,
username: 'benjaminscott',
gender: 0,
mobile: '13599998888',
email: 'benjaminscott@company.com',
dep: '研发部',
status: '1',
create_time: '2020-11-14 12:01:20',
avatar: avatar9
},
{
id: 15,
username: 'ameliaking',
gender: 1,
mobile: '13799998888',
email: 'ameliaking@company.com',
dep: '研发部',
status: '1',
create_time: '2020-11-14 12:01:20',
avatar: avatar10
}
]
export interface Role {
roleName: string
roleCode: string
des: string
date: string
enable: boolean
}
// 角色列表
export const ROLE_LIST_DATA: Role[] = [
{
roleName: '超级管理员',
roleCode: 'R_SUPER',
des: '拥有系统全部权限',
date: '2025-05-15 12:30:45',
enable: true
},
{
roleName: '管理员',
roleCode: 'R_ADMIN',
des: '拥有系统管理权限',
date: '2025-05-15 12:30:45',
enable: true
},
{
roleName: '普通用户',
roleCode: 'R_USER',
des: '拥有系统普通权限',
date: '2025-05-15 12:30:45',
enable: true
},
{
roleName: '财务管理员',
roleCode: 'R_FINANCE',
des: '管理财务相关权限',
date: '2025-05-16 09:15:30',
enable: true
},
{
roleName: '数据分析师',
roleCode: 'R_ANALYST',
des: '拥有数据分析权限',
date: '2025-05-16 11:45:00',
enable: false
},
{
roleName: '客服专员',
roleCode: 'R_SUPPORT',
des: '处理客户支持请求',
date: '2025-05-17 14:30:22',
enable: true
},
{
roleName: '营销经理',
roleCode: 'R_MARKETING',
des: '管理营销活动权限',
date: '2025-05-17 15:10:50',
enable: true
},
{
roleName: '访客用户',
roleCode: 'R_GUEST',
des: '仅限浏览权限',
date: '2025-05-18 08:25:40',
enable: false
},
{
roleName: '系统维护员',
roleCode: 'R_MAINTAINER',
des: '负责系统维护和更新',
date: '2025-05-18 09:50:12',
enable: true
},
{
roleName: '项目经理',
roleCode: 'R_PM',
des: '管理项目相关权限',
date: '2025-05-19 13:40:35',
enable: true
}
]

View File

@@ -5,7 +5,6 @@ import { useUserStore } from '@/store/modules/user'
import { useMenuStore } from '@/store/modules/menu' import { useMenuStore } from '@/store/modules/menu'
import { setWorktab } from '@/utils/navigation' import { setWorktab } from '@/utils/navigation'
import { setPageTitle, setSystemTheme } from '../utils/utils' import { setPageTitle, setSystemTheme } from '../utils/utils'
import { menuService } from '@/api/menuApi'
import { registerDynamicRoutes } from '../utils/registerRoutes' import { registerDynamicRoutes } from '../utils/registerRoutes'
import { AppRouteRecord } from '@/types/router' import { AppRouteRecord } from '@/types/router'
import { RoutesAlias } from '../routesAlias' import { RoutesAlias } from '../routesAlias'

View File

@@ -805,15 +805,6 @@ export const asyncRoutes: AppRouteRecord[] = [
// } // }
// }, // },
// { // {
// path: 'chat',
// name: 'Chat',
// component: RoutesAlias.Chat,
// meta: {
// title: 'menus.template.chat',
// keepAlive: true
// }
// },
// {
// path: 'calendar', // path: 'calendar',
// name: 'Calendar', // name: 'Calendar',
// component: RoutesAlias.Calendar, // component: RoutesAlias.Calendar,

View File

@@ -99,7 +99,6 @@ export enum RoutesAlias {
ContextMenu = '/widgets/context-menu', // 上下文菜单 ContextMenu = '/widgets/context-menu', // 上下文菜单
Qrcode = '/widgets/qrcode', // 二维码 Qrcode = '/widgets/qrcode', // 二维码
Drag = '/widgets/drag', // 拖拽 Drag = '/widgets/drag', // 拖拽
Chat = '/template/chat', // 聊天
Charts = '/template/charts', // 图表 Charts = '/template/charts', // 图表
Map = '/template/map', // 地图 Map = '/template/map', // 地图
Calendar = '/template/calendar' // 日历 Calendar = '/template/calendar' // 日历

View File

@@ -1,63 +0,0 @@
/**
* 客户账号管理相关类型定义
*/
// 客户账号信息
export interface CustomerAccountItem {
id: number
username: string
phone: string
user_type: number // 3=代理账号, 4=企业账号
user_type_name: string
shop_id: number | null
shop_name: string
enterprise_id: number | null
enterprise_name: string
status: number // 0=禁用, 1=启用
status_name: string
created_at: string
}
// 客户账号分页结果
export interface CustomerAccountPageResult {
items: CustomerAccountItem[]
page: number
size: number
total: number
}
// 查询客户账号列表参数
export interface CustomerAccountQueryParams {
page?: number
page_size?: number
username?: string
phone?: string
user_type?: number | null
shop_id?: number | null
enterprise_id?: number | null
status?: number | null
}
// 新增代理商账号参数
export interface CreateCustomerAccountParams {
username: string
phone: string
password: string
shop_id: number
}
// 编辑账号参数
export interface UpdateCustomerAccountParams {
username?: string
phone?: string
}
// 修改账号密码参数
export interface UpdateCustomerAccountPasswordParams {
password: string
}
// 修改账号状态参数
export interface UpdateCustomerAccountStatusParams {
status: 0 | 1
}

View File

@@ -17,12 +17,6 @@ export * from './permission'
// 账号相关 // 账号相关
export * from './account' export * from './account'
// 平台账号相关
export * from './platformAccount'
// 代理账号相关
export * from './shopAccount'
// 店铺相关 // 店铺相关
export * from './shop' export * from './shop'
@@ -56,9 +50,6 @@ export * from './commission'
// 企业客户相关 // 企业客户相关
export * from './enterprise' export * from './enterprise'
// 客户账号相关
export * from './customerAccount'
// 设置相关 // 设置相关
export * from './setting' export * from './setting'

View File

@@ -1,82 +0,0 @@
/**
* 平台账号相关类型定义 - 匹配后端 API 文档
*/
import { PaginationParams } from './common'
// 平台账号响应实体(匹配后端 ModelAccountResponse - 使用大写字段名)
export interface PlatformAccountResponse {
ID: number // 账号ID
CreatedAt?: string // 创建时间
UpdatedAt?: string // 更新时间
DeletedAt?: string | null // 删除时间
creator?: number // 创建人ID
updater?: number // 更新人ID
username: string // 用户名
phone: string // 手机号
user_type: number // 用户类型 (1:超级管理员, 2:平台用户, 3:代理账号, 4:企业账号)
enterprise_id?: number | null // 关联企业ID
shop_id?: number | null // 关联店铺ID
status: number // 状态 (0:禁用, 1:启用)
}
// 平台账号列表查询参数
export interface PlatformAccountQueryParams extends PaginationParams {
username?: string // 用户名模糊查询
phone?: string // 手机号模糊查询
status?: number | null // 状态 (0:禁用, 1:启用)
page?: number // 页码
page_size?: number // 每页数量
}
// 创建平台账号参数(匹配后端 ModelCreateAccountRequest
export interface CreatePlatformAccountParams {
username: string // 用户名
password: string // 密码
phone: string // 手机号
user_type: number // 用户类型 (1:超级管理员, 2:平台用户, 3:代理账号, 4:企业账号)
enterprise_id?: number | null // 关联企业ID企业账号必填
shop_id?: number | null // 关联店铺ID代理账号必填
}
// 更新平台账号参数(匹配后端 ModelUpdateAccountParams
export interface UpdatePlatformAccountParams {
username?: string | null // 用户名
password?: string | null // 密码
phone?: string | null // 手机号
status?: number | null // 状态 (0:禁用, 1:启用)
}
// 修改密码参数
export interface ChangePlatformAccountPasswordParams {
new_password: string // 新密码
}
// 分配角色参数
export interface AssignRolesParams {
role_ids: number[] // 角色ID列表
}
// 启用/禁用账号参数
export interface UpdateAccountStatusParams {
status: number // 状态 (0:禁用, 1:启用)
}
// 平台账号角色响应
export interface PlatformAccountRoleResponse {
creator?: number // 创建人ID
role_desc?: string // 角色描述
role_name?: string // 角色名称
role_type?: number // 角色类型
status?: number // 状态
updater?: number // 更新人ID
ID?: number // 角色ID
}
// 平台账号列表分页响应(匹配后端 ModelAccountPageResult
export interface PlatformAccountPageResult {
items: PlatformAccountResponse[] | null // 账号列表
page?: number // 当前页码
size?: number // 每页数量
total?: number // 总记录数
}

View File

@@ -1,58 +0,0 @@
/**
* 代理账号相关类型定义 - 匹配后端 API 文档
*/
import { PaginationParams } from './common'
// 代理账号响应实体根据API文档使用小写字段名
export interface ShopAccountResponse {
id: number // 账号ID
created_at: string // 创建时间
updated_at: string // 更新时间
username: string // 用户名
phone: string // 手机号
shop_id: number // 店铺ID
user_type: number // 用户类型 (1:超级管理员, 2:平台用户, 3:代理账号, 4:企业账号)
status: number // 状态 (0:禁用, 1:启用)
}
// 代理账号列表查询参数
export interface ShopAccountQueryParams extends PaginationParams {
username?: string // 用户名模糊查询
phone?: string // 手机号精确查询
shop_id?: number | null // 店铺ID过滤
status?: number | null // 状态 (0:禁用, 1:启用)
page?: number // 页码
page_size?: number // 每页数量
}
// 创建代理账号参数
export interface CreateShopAccountParams {
username: string // 用户名
password: string // 密码
phone: string // 手机号
shop_id: number // 店铺ID
}
// 更新代理账号参数
export interface UpdateShopAccountParams {
username: string // 用户名
}
// 重置密码参数
export interface UpdateShopAccountPasswordParams {
new_password: string // 新密码
}
// 更新状态参数
export interface UpdateShopAccountStatusParams {
status: number // 状态 (0:禁用, 1:启用)
}
// 代理账号列表分页响应
export interface ShopAccountPageResult {
items: ShopAccountResponse[] | null // 账号列表
page?: number // 当前页码
size?: number // 每页数量
total?: number // 总记录数
}

View File

@@ -8,7 +8,7 @@ export {}
declare global { declare global {
const EffectScope: typeof import('vue')['EffectScope'] const EffectScope: typeof import('vue')['EffectScope']
const ElButton: (typeof import('element-plus/es'))['ElButton'] const ElButton: (typeof import('element-plus/es'))['ElButton']
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox'] const ElMessageBox: (typeof import('element-plus/es'))['ElMessageBox']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed'] const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef'] const autoResetRef: typeof import('@vueuse/core')['autoResetRef']

View File

@@ -15,7 +15,6 @@ declare module 'vue' {
ArtBreadcrumb: typeof import('./../components/core/layouts/art-breadcrumb/index.vue')['default'] ArtBreadcrumb: typeof import('./../components/core/layouts/art-breadcrumb/index.vue')['default']
ArtButtonMore: typeof import('./../components/core/forms/ArtButtonMore.vue')['default'] ArtButtonMore: typeof import('./../components/core/forms/ArtButtonMore.vue')['default']
ArtButtonTable: typeof import('./../components/core/forms/ArtButtonTable.vue')['default'] ArtButtonTable: typeof import('./../components/core/forms/ArtButtonTable.vue')['default']
ArtCardBanner: typeof import('./../components/core/banners/ArtCardBanner.vue')['default']
ArtChatWindow: typeof import('./../components/core/layouts/art-chat-window/index.vue')['default'] ArtChatWindow: typeof import('./../components/core/layouts/art-chat-window/index.vue')['default']
ArtCutterImg: typeof import('./../components/core/media/ArtCutterImg.vue')['default'] ArtCutterImg: typeof import('./../components/core/media/ArtCutterImg.vue')['default']
ArtDataListCard: typeof import('./../components/core/cards/ArtDataListCard.vue')['default'] ArtDataListCard: typeof import('./../components/core/cards/ArtDataListCard.vue')['default']
@@ -84,55 +83,7 @@ declare module 'vue' {
CreateRefundDialog: typeof import('./../components/business/CreateRefundDialog.vue')['default'] CreateRefundDialog: typeof import('./../components/business/CreateRefundDialog.vue')['default']
CustomerAccountDialog: typeof import('./../components/business/CustomerAccountDialog.vue')['default'] CustomerAccountDialog: typeof import('./../components/business/CustomerAccountDialog.vue')['default']
DetailPage: typeof import('./../components/common/DetailPage.vue')['default'] DetailPage: typeof import('./../components/common/DetailPage.vue')['default']
ElAlert: typeof import('element-plus/es')['ElAlert']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElButton: typeof import('element-plus/es')['ElButton']
ElCalendar: typeof import('element-plus/es')['ElCalendar']
ElCard: typeof import('element-plus/es')['ElCard']
ElCascader: typeof import('element-plus/es')['ElCascader']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCol: typeof import('element-plus/es')['ElCol']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
ElEmpty: typeof import('element-plus/es')['ElEmpty']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElRow: typeof import('element-plus/es')['ElRow']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
ElTag: typeof import('element-plus/es')['ElTag']
ElTimeline: typeof import('element-plus/es')['ElTimeline']
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
ElUpload: typeof import('element-plus/es')['ElUpload']
ElWatermark: typeof import('element-plus/es')['ElWatermark']
HorizontalSubmenu: typeof import('./../components/core/layouts/art-menus/art-horizontal-menu/widget/HorizontalSubmenu.vue')['default'] HorizontalSubmenu: typeof import('./../components/core/layouts/art-menus/art-horizontal-menu/widget/HorizontalSubmenu.vue')['default']
ImportDialog: typeof import('./../components/business/ImportDialog.vue')['default'] ImportDialog: typeof import('./../components/business/ImportDialog.vue')['default']
LoginLeftView: typeof import('./../components/core/views/login/LoginLeftView.vue')['default'] LoginLeftView: typeof import('./../components/core/views/login/LoginLeftView.vue')['default']
@@ -156,7 +107,4 @@ declare module 'vue' {
ThemeSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/ThemeSettings.vue')['default'] ThemeSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/ThemeSettings.vue')['default']
UpdateRealnameStatusDialog: typeof import('./../components/business/UpdateRealnameStatusDialog.vue')['default'] UpdateRealnameStatusDialog: typeof import('./../components/business/UpdateRealnameStatusDialog.vue')['default']
} }
export interface ComponentCustomProperties {
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
}
} }

View File

@@ -27,7 +27,6 @@ export interface MenuLayout {
export interface FestivalConfig { export interface FestivalConfig {
date: string date: string
name: string name: string
image: string
scrollText: string scrollText: string
isActive?: boolean isActive?: boolean
} }

View File

@@ -256,15 +256,12 @@ export function formatDateTime(
} }
} }
const parsedDate = new Date( const parsedDate = new Date(typeof date === 'number' ? normalizeTimestamp(date) : date)
typeof date === 'number' ? normalizeTimestamp(date) : date
)
if (Number.isNaN(parsedDate.getTime())) return '-' if (Number.isNaN(parsedDate.getTime())) return '-'
return formatDateTimeParts(getUtcDateTimeParts(parsedDate), format) return formatDateTimeParts(getUtcDateTimeParts(parsedDate), format)
} }
/** /**
* 格式化费率(基点 -> 百分比) * 格式化费率(基点 -> 百分比)
* @param basisPoints 费率基点100基点=1% * @param basisPoints 费率基点100基点=1%

View File

@@ -56,9 +56,6 @@ export const isMaskedPaymentConfigValue = (value: string | undefined | null): bo
if (!value) return false if (!value) return false
return ( return (
value === '***' || value === '***' || value.includes('已配置') || value.includes('未配置') || /\*{2,}/.test(value)
value.includes('已配置') ||
value.includes('未配置') ||
/\*{2,}/.test(value)
) )
} }

View File

@@ -86,7 +86,7 @@
:label="role.role_name" :label="role.role_name"
:value="role.ID" :value="role.ID"
> >
<div style="display: flex; justify-content: space-between; align-items: center"> <div style="display: flex; align-items: center; justify-content: space-between">
<span>{{ role.role_name }}</span> <span>{{ role.role_name }}</span>
<ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small"> <ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small">
{{ role.role_type === 1 ? '平台角色' : '客户角色' }} {{ role.role_type === 1 ? '平台角色' : '客户角色' }}
@@ -1010,13 +1010,13 @@
width: 100%; width: 100%;
.role-item { .role-item {
box-sizing: border-box;
display: block; display: block;
width: 100%; width: 100%;
padding: 10px 12px; padding: 10px 12px;
border: 1px solid var(--el-border-color-lighter); border: 1px solid var(--el-border-color-lighter);
border-radius: 4px; border-radius: 4px;
transition: all 0.2s; transition: all 0.2s;
box-sizing: border-box;
&:hover { &:hover {
background: var(--el-fill-color-light); background: var(--el-fill-color-light);
@@ -1026,8 +1026,8 @@
.role-info { .role-info {
display: flex; display: flex;
flex: 1; flex: 1;
justify-content: space-between;
align-items: center; align-items: center;
justify-content: space-between;
} }
:deep(.el-checkbox), :deep(.el-checkbox),

View File

@@ -251,8 +251,8 @@
} }
:deep(.el-card__header) .card-header { :deep(.el-card__header) .card-header {
align-items: stretch;
flex-direction: column; flex-direction: column;
align-items: stretch;
.el-button { .el-button {
width: 100%; width: 100%;
@@ -261,13 +261,13 @@
} }
.iccid-search { .iccid-search {
align-items: stretch;
flex-direction: column; flex-direction: column;
align-items: stretch;
.iccid-input-wrapper { .iccid-input-wrapper {
flex-basis: auto; flex-basis: auto;
min-width: 0;
width: 100%; width: 100%;
min-width: 0;
.iccid-magnifier { .iccid-magnifier {
right: 0; right: 0;

View File

@@ -29,7 +29,9 @@
<!-- 卡专属字段 --> <!-- 卡专属字段 -->
<template v-if="cardInfo?.asset_type === 'card'"> <template v-if="cardInfo?.asset_type === 'card'">
<ElDescriptionsItem label="ICCID">{{ cardInfo?.iccid || '-' }}</ElDescriptionsItem> <ElDescriptionsItem label="ICCID">{{ cardInfo?.iccid || '-' }}</ElDescriptionsItem>
<ElDescriptionsItem label="IMEI">{{ cardInfo?.gateway_card_imei || '-' }}</ElDescriptionsItem> <ElDescriptionsItem label="IMEI">{{
cardInfo?.gateway_card_imei || '-'
}}</ElDescriptionsItem>
<ElDescriptionsItem label="运营商账户">{{ <ElDescriptionsItem label="运营商账户">{{
cardInfo?.carrier_name || '-' cardInfo?.carrier_name || '-'
}}</ElDescriptionsItem> }}</ElDescriptionsItem>
@@ -182,7 +184,7 @@
<el-icon <el-icon
class="copy-icon" class="copy-icon"
@click="handleCopyICCID(scope.row.iccid)" @click="handleCopyICCID(scope.row.iccid)"
style="cursor: pointer; color: var(--el-color-primary)" style=" color: var(--el-color-primary);cursor: pointer"
> >
<CopyDocument /> <CopyDocument />
</el-icon> </el-icon>
@@ -844,8 +846,8 @@
@media (width <= 768px) { @media (width <= 768px) {
.card-header { .card-header {
align-items: flex-start;
flex-direction: column; flex-direction: column;
align-items: flex-start;
.card-header-left, .card-header-left,
.card-header-right { .card-header-right {

View File

@@ -193,7 +193,7 @@
<ElDescriptionsItem label="套餐名称"> <ElDescriptionsItem label="套餐名称">
{{ currentPackage.package_name || '-' }} {{ currentPackage.package_name || '-' }}
</ElDescriptionsItem> </ElDescriptionsItem>
<ElDescriptionsItem label="套餐成本价格" v-if="isAdminOrPlatform"> <ElDescriptionsItem label="套餐成本价格" v-if="isAdminOrPlatform">
{{ formatAmount(currentPackage.paid_amount || 0) }} {{ formatAmount(currentPackage.paid_amount || 0) }}
</ElDescriptionsItem> </ElDescriptionsItem>
<ElDescriptionsItem label="套餐零售价"> <ElDescriptionsItem label="套餐零售价">
@@ -285,7 +285,8 @@
hasAuth('asset_info:view_current_package_virtual_usage') hasAuth('asset_info:view_current_package_virtual_usage')
) )
const canShowCurrentPackageVirtualUsage = computed( const canShowCurrentPackageVirtualUsage = computed(
() => canViewCurrentPackageVirtualUsage.value && props.currentPackage?.enable_virtual_data !== false () =>
canViewCurrentPackageVirtualUsage.value && props.currentPackage?.enable_virtual_data !== false
) )
const shouldShowTrafficCard = computed(() => { const shouldShowTrafficCard = computed(() => {
if (!isAdminOrPlatform.value) return true if (!isAdminOrPlatform.value) return true
@@ -322,11 +323,11 @@
<style scoped lang="scss"> <style scoped lang="scss">
.current-package-card { .current-package-card {
.card-header { .card-header {
display: flex; display: flex;
gap: 12px; gap: 12px;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
.header-title { .header-title {
font-size: 16px; font-size: 16px;
@@ -336,24 +337,24 @@
.flow-progress-card { .flow-progress-card {
padding: 20px; padding: 20px;
margin-bottom: 20px;
background: var(--el-fill-color-light); background: var(--el-fill-color-light);
border-radius: 8px; border-radius: 8px;
margin-bottom: 20px;
.flow-stats-container { .flow-stats-container {
display: flex; display: flex;
gap: 40px; gap: 40px;
min-width: 0; min-width: 0;
.flow-stat-section { .flow-stat-section {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
.flow-stat-label { .flow-stat-label {
font-size: 13px;
color: var(--el-text-color-secondary);
margin-bottom: 8px; margin-bottom: 8px;
font-size: 13px;
font-weight: 500; font-weight: 500;
color: var(--el-text-color-secondary);
} }
.flow-stat-row { .flow-stat-row {
@@ -364,8 +365,8 @@
.stat-item { .stat-item {
display: flex; display: flex;
align-items: baseline;
gap: 4px; gap: 4px;
align-items: baseline;
.stat-label { .stat-label {
font-size: 12px; font-size: 12px;
@@ -390,8 +391,8 @@
.flow-stat-item { .flow-stat-item {
display: flex; display: flex;
align-items: baseline;
gap: 8px; gap: 8px;
align-items: baseline;
.flow-stat-label { .flow-stat-label {
font-size: 13px; font-size: 13px;
@@ -410,16 +411,16 @@
padding: 0 4px; padding: 0 4px;
.progress-label { .progress-label {
margin-bottom: 4px;
font-size: 12px; font-size: 12px;
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
margin-bottom: 4px;
} }
} }
.progress-wrapper { .progress-wrapper {
display: flex; display: flex;
align-items: center;
gap: 1px; gap: 1px;
align-items: center;
.progress-track { .progress-track {
position: relative; position: relative;
@@ -429,13 +430,13 @@
.breakpoint-marker { .breakpoint-marker {
position: absolute; position: absolute;
top: 50%; top: 50%;
z-index: 1;
width: 8px; width: 8px;
height: 8px; height: 8px;
cursor: pointer;
background-color: var(--el-color-danger); background-color: var(--el-color-danger);
border-radius: 50%; border-radius: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
z-index: 1;
cursor: pointer;
} }
} }
@@ -472,8 +473,8 @@
@media (width <= 768px) { @media (width <= 768px) {
.card-header { .card-header {
align-items: flex-start;
flex-direction: column; flex-direction: column;
align-items: flex-start;
.header-actions { .header-actions {
width: 100%; width: 100%;
@@ -490,9 +491,9 @@
} }
.progress-wrapper { .progress-wrapper {
align-items: stretch;
flex-direction: column; flex-direction: column;
gap: 6px; gap: 6px;
align-items: stretch;
.progress-percentage { .progress-percentage {
min-width: 0; min-width: 0;

View File

@@ -604,10 +604,10 @@
.operation-logs-card { .operation-logs-card {
.card-header { .card-header {
display: flex; display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap; flex-wrap: wrap;
gap: 12px; gap: 12px;
align-items: center;
justify-content: space-between;
.header-title { .header-title {
font-size: 16px; font-size: 16px;
@@ -616,9 +616,9 @@
.filter-section { .filter-section {
display: flex; display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px;
align-items: center;
justify-content: flex-end; justify-content: flex-end;
min-width: 0; min-width: 0;
@@ -651,20 +651,20 @@
.change-item { .change-item {
display: flex; display: flex;
align-items: center;
gap: 4px; gap: 4px;
align-items: center;
font-size: 12px; font-size: 12px;
.field-name { .field-name {
color: var(--el-text-color-secondary);
min-width: 72px; min-width: 72px;
color: var(--el-text-color-secondary);
} }
.field-value { .field-value {
display: flex; display: flex;
align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
gap: 4px; gap: 4px;
align-items: center;
word-break: break-all; word-break: break-all;
.new-value { .new-value {
@@ -691,8 +691,8 @@
@media (width <= 1200px) { @media (width <= 1200px) {
.card-header { .card-header {
align-items: stretch;
flex-direction: column; flex-direction: column;
align-items: stretch;
.filter-section { .filter-section {
justify-content: flex-start; justify-content: flex-start;
@@ -703,12 +703,12 @@
@media (width <= 768px) { @media (width <= 768px) {
.card-header { .card-header {
align-items: stretch;
flex-direction: column; flex-direction: column;
align-items: stretch;
.filter-section { .filter-section {
align-items: stretch;
flex-direction: column; flex-direction: column;
align-items: stretch;
:deep(.el-select), :deep(.el-select),
.el-button, .el-button,
@@ -723,9 +723,9 @@
} }
.logs-table-wrapper { .logs-table-wrapper {
padding: 0 12px;
margin-right: -12px; margin-right: -12px;
margin-left: -12px; margin-left: -12px;
padding: 0 12px;
:deep(.el-pagination) { :deep(.el-pagination) {
justify-content: flex-start !important; justify-content: flex-start !important;

View File

@@ -444,8 +444,8 @@
.package-table-wrapper { .package-table-wrapper {
.table-scroll-container { .table-scroll-container {
overflow-x: auto;
width: 100%; width: 100%;
overflow-x: auto;
} }
.package-table { .package-table {
@@ -459,9 +459,9 @@
.package-name-text { .package-name-text {
display: block; display: block;
overflow: hidden; overflow: hidden;
font-weight: 500;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
font-weight: 500;
} }
.traffic-progress { .traffic-progress {
@@ -489,8 +489,8 @@
.progress-wrapper { .progress-wrapper {
display: flex; display: flex;
align-items: center;
gap: 1px; gap: 1px;
align-items: center;
.progress-track { .progress-track {
position: relative; position: relative;
@@ -500,13 +500,13 @@
.breakpoint-marker { .breakpoint-marker {
position: absolute; position: absolute;
top: 50%; top: 50%;
z-index: 1;
width: 8px; width: 8px;
height: 8px; height: 8px;
cursor: pointer;
background-color: var(--el-color-danger); background-color: var(--el-color-danger);
border-radius: 50%; border-radius: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
z-index: 1;
cursor: pointer;
} }
} }
@@ -523,15 +523,15 @@
} }
.pagination { .pagination {
margin-top: 16px;
justify-content: flex-end; justify-content: flex-end;
margin-top: 16px;
} }
@media (width <= 768px) { @media (width <= 768px) {
.card-header { .card-header {
align-items: stretch;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
align-items: stretch;
.card-header-right, .card-header-right,
.card-header-right .el-button { .card-header-right .el-button {
@@ -546,15 +546,15 @@
.traffic-progress { .traffic-progress {
.progress-text { .progress-text {
align-items: flex-start;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 4px;
align-items: flex-start;
} }
.progress-wrapper { .progress-wrapper {
align-items: stretch;
flex-direction: column; flex-direction: column;
gap: 6px; gap: 6px;
align-items: stretch;
.progress-percentage { .progress-percentage {
min-width: 0; min-width: 0;

View File

@@ -421,8 +421,8 @@
@media (width <= 1200px) { @media (width <= 1200px) {
.wallet-header { .wallet-header {
align-items: flex-start;
flex-direction: column; flex-direction: column;
align-items: flex-start;
.header-left, .header-left,
.filter-section { .filter-section {
@@ -437,8 +437,8 @@
@media (width <= 768px) { @media (width <= 768px) {
.wallet-header { .wallet-header {
align-items: stretch;
flex-direction: column; flex-direction: column;
align-items: stretch;
.header-left, .header-left,
.filter-section { .filter-section {
@@ -446,8 +446,8 @@
} }
.filter-section { .filter-section {
align-items: stretch;
flex-direction: column; flex-direction: column;
align-items: stretch;
:deep(.el-select), :deep(.el-select),
:deep(.el-date-editor), :deep(.el-date-editor),
@@ -462,9 +462,9 @@
} }
.header-left { .header-left {
align-items: flex-start;
flex-direction: column; flex-direction: column;
gap: 6px; gap: 6px;
align-items: flex-start;
.wallet-divider { .wallet-divider {
display: none; display: none;
@@ -490,9 +490,9 @@
@media (width <= 480px) { @media (width <= 480px) {
.wallet-table-wrapper { .wallet-table-wrapper {
padding: 0 12px;
margin-right: -12px; margin-right: -12px;
margin-left: -12px; margin-left: -12px;
padding: 0 12px;
} }
} }
} }

View File

@@ -44,17 +44,17 @@
</ElOption> </ElOption>
</ElSelect> </ElSelect>
<template v-if="noSeriesId"> <template v-if="noSeriesId">
<div style="color: var(--el-text-color-warning); font-size: 12px; margin-top: 4px"> <div style=" margin-top: 4px; font-size: 12px;color: var(--el-text-color-warning)">
该设备未关联套餐系列无法购买套餐 该设备未关联套餐系列无法购买套餐
</div> </div>
</template> </template>
<template v-else-if="packagesEmptyWithSeriesId"> <template v-else-if="packagesEmptyWithSeriesId">
<div style="color: var(--el-text-color-info); font-size: 12px; margin-top: 4px"> <div style=" margin-top: 4px; font-size: 12px;color: var(--el-text-color-info)">
该系列暂无可用套餐 该系列暂无可用套餐
</div> </div>
</template> </template>
<template v-else-if="selectedPackageIsGift"> <template v-else-if="selectedPackageIsGift">
<div style="color: var(--el-color-warning); font-size: 12px; margin-top: 4px"> <div style=" margin-top: 4px; font-size: 12px;color: var(--el-color-warning)">
提示: 赠送套餐只能使用线下支付方式且必须上传支付凭证 提示: 赠送套餐只能使用线下支付方式且必须上传支付凭证
</div> </div>
</template> </template>
@@ -95,9 +95,7 @@
:limit="1" :limit="1"
> >
<el-icon class="payment-voucher-upload__icon"><UploadFilled /></el-icon> <el-icon class="payment-voucher-upload__icon"><UploadFilled /></el-icon>
<div class="payment-voucher-upload__text"> <div class="payment-voucher-upload__text"> 将支付凭证拖到此处<em>点击上传</em> </div>
将支付凭证拖到此处<em>点击上传</em>
</div>
<template #tip> <template #tip>
<div class="el-upload__tip">支持 jpgpng 格式文件大小不超过 5MB</div> <div class="el-upload__tip">支持 jpgpng 格式文件大小不超过 5MB</div>
</template> </template>
@@ -459,8 +457,8 @@
<style lang="scss" scoped> <style lang="scss" scoped>
.dialog-footer { .dialog-footer {
display: flex; display: flex;
justify-content: flex-end;
gap: 12px; gap: 12px;
justify-content: flex-end;
} }
.payment-voucher-upload { .payment-voucher-upload {
@@ -487,8 +485,8 @@
color: var(--el-text-color-regular); color: var(--el-text-color-regular);
em { em {
color: var(--el-color-primary);
font-style: normal; font-style: normal;
color: var(--el-color-primary);
} }
} }
} }

View File

@@ -245,7 +245,7 @@ export function useAssetInfo() {
order_no: pkg.order_no || '', order_no: pkg.order_no || '',
package_name: pkg.package_name || '', package_name: pkg.package_name || '',
paid_amount: pkg.paid_amount || 0, paid_amount: pkg.paid_amount || 0,
retail_amount : pkg.retail_amount || 0, retail_amount: pkg.retail_amount || 0,
real_total_mb: pkg.real_total_mb || 0, real_total_mb: pkg.real_total_mb || 0,
real_used_mb: pkg.real_used_mb || 0, real_used_mb: pkg.real_used_mb || 0,
real_remaining_mb: (pkg.real_total_mb || 0) - (pkg.real_used_mb || 0), real_remaining_mb: (pkg.real_total_mb || 0) - (pkg.real_used_mb || 0),

View File

@@ -690,8 +690,8 @@
} }
.single-card-page { .single-card-page {
padding: 20px;
max-width: 100%; max-width: 100%;
padding: 20px;
overflow-x: hidden; overflow-x: hidden;
// 资产查询卡片 // 资产查询卡片
@@ -836,8 +836,8 @@
.row { .row {
display: flex; display: flex;
gap: 24px; gap: 24px;
min-width: 0;
width: 100%; width: 100%;
min-width: 0;
&.full-width { &.full-width {
.info-card { .info-card {
@@ -910,10 +910,10 @@
:deep(.el-card__header) { :deep(.el-card__header) {
padding: 20px 24px 16px; padding: 20px 24px 16px;
.card-header { .card-header {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px; gap: 8px;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
font-size: 16px; font-size: 16px;
@@ -946,8 +946,8 @@
height: 26px; height: 26px;
.el-switch__core { .el-switch__core {
height: 26px;
min-width: 50px; min-width: 50px;
height: 26px;
.el-switch__inner { .el-switch__inner {
font-size: 13px; font-size: 13px;

File diff suppressed because it is too large Load Diff

View File

@@ -86,7 +86,7 @@
<!-- 卡列表 --> <!-- 卡列表 -->
<div <div
class="card-selection-info" class="card-selection-info"
style="display: flex; justify-content: space-between; align-items: center" style="display: flex; align-items: center; justify-content: space-between"
> >
<span>已选择 {{ selectedAvailableCards.length }} 张卡</span> <span>已选择 {{ selectedAvailableCards.length }} 张卡</span>
<ElButton <ElButton
@@ -1312,6 +1312,7 @@
.enterprise-cards-page { .enterprise-cards-page {
.enterprise-info-card { .enterprise-info-card {
margin-bottom: 16px; margin-bottom: 16px;
:deep(.el-card__body) { :deep(.el-card__body) {
display: none; display: none;
} }
@@ -1319,8 +1320,8 @@
.card-header { .card-header {
display: flex; display: flex;
align-items: center;
gap: 12px; gap: 12px;
align-items: center;
} }
.card-selection-info { .card-selection-info {

View File

@@ -87,7 +87,7 @@
<!-- 设备列表 --> <!-- 设备列表 -->
<div <div
class="device-selection-info" class="device-selection-info"
style="display: flex; justify-content: space-between; align-items: center" style="display: flex; align-items: center; justify-content: space-between"
> >
<span>已选择 {{ selectedAvailableDevices.length }} 台设备</span> <span>已选择 {{ selectedAvailableDevices.length }} 台设备</span>
<ElButton <ElButton
@@ -1057,12 +1057,13 @@
.enterprise-devices-page { .enterprise-devices-page {
.card-header { .card-header {
display: flex; display: flex;
align-items: center;
gap: 12px; gap: 12px;
align-items: center;
} }
.enterprise-info-card { .enterprise-info-card {
margin-bottom: 16px; margin-bottom: 16px;
:deep(.el-card__body) { :deep(.el-card__body) {
display: none; display: none;
} }

View File

@@ -73,7 +73,8 @@
}, },
{ {
label: '流程类型', label: '流程类型',
formatter: (_, data) => data.flow_type_name || (flowType === 'direct' ? '直接换货' : '物流换货') formatter: (_, data) =>
data.flow_type_name || (flowType === 'direct' ? '直接换货' : '物流换货')
}, },
{ {
label: '换货原因', label: '换货原因',

View File

@@ -514,7 +514,8 @@
prop: 'flow_type', prop: 'flow_type',
label: '流程类型', label: '流程类型',
width: 120, width: 120,
formatter: (row: any) => row.flow_type_name || (row.flow_type === 'direct' ? '直接换货' : '物流换货') formatter: (row: any) =>
row.flow_type_name || (row.flow_type === 'direct' ? '直接换货' : '物流换货')
}, },
{ {
prop: 'old_asset_type', prop: 'old_asset_type',

File diff suppressed because it is too large Load Diff

View File

@@ -957,8 +957,8 @@
.asset-identifier-link { .asset-identifier-link {
color: var(--el-color-primary); color: var(--el-color-primary);
cursor: pointer;
text-decoration: underline; text-decoration: underline;
cursor: pointer;
} }
:deep(.el-table__row.table-row-with-context-menu) { :deep(.el-table__row.table-row-with-context-menu) {

View File

@@ -822,13 +822,13 @@
width: 100%; width: 100%;
.role-item { .role-item {
box-sizing: border-box;
display: block; display: block;
width: 100%; width: 100%;
padding: 10px 12px; padding: 10px 12px;
border: 1px solid var(--el-border-color-lighter); border: 1px solid var(--el-border-color-lighter);
border-radius: 4px; border-radius: 4px;
transition: all 0.2s; transition: all 0.2s;
box-sizing: border-box;
&:hover { &:hover {
background: var(--el-fill-color-light); background: var(--el-fill-color-light);
@@ -838,8 +838,8 @@
.role-info { .role-info {
display: flex; display: flex;
flex: 1; flex: 1;
justify-content: space-between;
align-items: center; align-items: center;
justify-content: space-between;
} }
:deep(.el-checkbox), :deep(.el-checkbox),

View File

@@ -222,17 +222,17 @@
.stats-chart { .stats-chart {
display: flex; display: flex;
align-items: flex-end;
gap: 8px; gap: 8px;
align-items: flex-end;
height: 120px; height: 120px;
padding-top: 10px; padding-top: 10px;
.chart-bar { .chart-bar {
flex: 1;
display: flex; display: flex;
flex: 1;
flex-direction: column; flex-direction: column;
align-items: center;
gap: 4px; gap: 4px;
align-items: center;
.bar-label { .bar-label {
font-size: 11px; font-size: 11px;
@@ -240,16 +240,16 @@
} }
.bar-value { .bar-value {
width: 100%;
display: flex; display: flex;
justify-content: center; justify-content: center;
width: 100%;
.bar-fill { .bar-fill {
width: 80%; width: 80%;
max-width: 24px; max-width: 24px;
cursor: pointer;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 4px 4px 0 0; border-radius: 4px 4px 0 0;
cursor: pointer;
transition: opacity 0.2s; transition: opacity 0.2s;
&:hover { &:hover {
@@ -292,8 +292,8 @@
} }
.stats-chart { .stats-chart {
overflow-x: auto;
padding-bottom: 10px; padding-bottom: 10px;
overflow-x: auto;
} }
} }
} }

View File

@@ -24,7 +24,7 @@
<el-table-column label="头像" prop="avatar" width="150px"> <el-table-column label="头像" prop="avatar" width="150px">
<template #default="scope"> <template #default="scope">
<div style="display: flex; align-items: center"> <div style="display: flex; align-items: center">
<img class="avatar" :src="scope.row.avatar" /> <span class="avatar">{{ scope.row.username.slice(0, 1) }}</span>
<span class="user-name">{{ scope.row.username }}</span> <span class="user-name">{{ scope.row.username }}</span>
</div> </div>
</template> </template>
@@ -49,12 +49,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref, reactive } from 'vue-demi' import { onMounted, ref, reactive } from 'vue-demi'
import avatar1 from '@/assets/img/avatar/avatar1.webp'
import avatar2 from '@/assets/img/avatar/avatar2.webp'
import avatar3 from '@/assets/img/avatar/avatar3.webp'
import avatar4 from '@/assets/img/avatar/avatar4.webp'
import avatar5 from '@/assets/img/avatar/avatar5.webp'
import avatar6 from '@/assets/img/avatar/avatar6.webp'
const radio2 = ref('本月') const radio2 = ref('本月')
@@ -66,8 +60,7 @@
age: 22, age: 22,
percentage: 60, percentage: 60,
pro: 0, pro: 0,
color: 'rgb(var(--art-primary)) !important', color: 'rgb(var(--art-primary)) !important'
avatar: avatar1
}, },
{ {
username: '何小荷', username: '何小荷',
@@ -76,8 +69,7 @@
age: 21, age: 21,
percentage: 20, percentage: 20,
pro: 0, pro: 0,
color: 'rgb(var(--art-secondary)) !important', color: 'rgb(var(--art-secondary)) !important'
avatar: avatar2
}, },
{ {
username: '誶誶淰', username: '誶誶淰',
@@ -86,8 +78,7 @@
age: 23, age: 23,
percentage: 60, percentage: 60,
pro: 0, pro: 0,
color: 'rgb(var(--art-warning)) !important', color: 'rgb(var(--art-warning)) !important'
avatar: avatar3
}, },
{ {
username: '发呆草', username: '发呆草',
@@ -96,8 +87,7 @@
age: 28, age: 28,
percentage: 50, percentage: 50,
pro: 0, pro: 0,
color: 'rgb(var(--art-info)) !important', color: 'rgb(var(--art-info)) !important'
avatar: avatar4
}, },
{ {
username: '甜筒', username: '甜筒',
@@ -106,8 +96,7 @@
age: 26, age: 26,
percentage: 70, percentage: 70,
pro: 0, pro: 0,
color: 'rgb(var(--art-error)) !important', color: 'rgb(var(--art-error)) !important'
avatar: avatar5
}, },
{ {
username: '冷月呆呆', username: '冷月呆呆',
@@ -116,8 +105,7 @@
age: 25, age: 25,
percentage: 90, percentage: 90,
pro: 0, pro: 0,
color: 'rgb(var(--art-success)) !important', color: 'rgb(var(--art-success)) !important'
avatar: avatar6
} }
]) ])
@@ -164,8 +152,14 @@
} }
.avatar { .avatar {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px; width: 36px;
height: 36px; height: 36px;
font-weight: 600;
color: #fff;
background: rgb(var(--art-primary));
border-radius: 6px; border-radius: 6px;
} }

View File

@@ -47,14 +47,6 @@
<!-- </el-col>--> <!-- </el-col>-->
<!--</el-row>--> <!--</el-row>-->
<!--<el-row :gutter="20">-->
<!-- <el-col :md="24" :lg="8">-->
<!-- <RecentTransaction />-->
<!-- </el-col>-->
<!-- <el-col :md="24" :lg="16" class="no-margin-bottom">-->
<!-- <HotProductsList />-->
<!-- </el-col>-->
<!--</el-row>-->
</div> </div>
</template> </template>
@@ -71,7 +63,6 @@
import ProductSales from './widget/ProductSales.vue' import ProductSales from './widget/ProductSales.vue'
import SalesGrowth from './widget/SalesGrowth.vue' import SalesGrowth from './widget/SalesGrowth.vue'
import CartConversionRate from './widget/CartConversionRate.vue' import CartConversionRate from './widget/CartConversionRate.vue'
import HotProductsList from './widget/HotProductsList.vue'
defineOptions({ name: 'Ecommerce' }) defineOptions({ name: 'Ecommerce' })
</script> </script>

View File

@@ -1,231 +0,0 @@
<template>
<div class="card art-custom-card" style="height: 27.8rem">
<div class="card-header">
<p class="title">热销产品</p>
<p class="subtitle">本月销售情况</p>
</div>
<div class="table">
<el-scrollbar style="height: 21.55rem">
<art-table
:data="tableData"
:pagination="false"
style="margin-top: 0 !important"
size="large"
:border="false"
:stripe="false"
:show-header-background="false"
>
<template #default>
<el-table-column label="产品" prop="product" width="220px">
<template #default="scope">
<div style="display: flex; align-items: center">
<img class="product-image" :src="scope.row.image" />
<div class="product-info">
<div class="product-name">{{ scope.row.name }}</div>
<div class="product-category">{{ scope.row.category }}</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="价格" prop="price">
<template #default="scope">
<span class="price">¥{{ scope.row.price.toLocaleString() }}</span>
</template>
</el-table-column>
<el-table-column label="库存" prop="stock">
<template #default="scope">
<div class="stock-badge" :class="getStockClass(scope.row.stock)">
{{ getStockStatus(scope.row.stock) }}
</div>
</template>
</el-table-column>
<el-table-column label="销量" prop="sales" />
<el-table-column label="销售趋势" width="240">
<template #default="scope">
<el-progress
:percentage="scope.row.pro"
:color="scope.row.color"
:stroke-width="4"
/>
</template>
</el-table-column>
</template>
</art-table>
</el-scrollbar>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, onMounted } from 'vue'
// 导入产品图片
import product1 from '@/assets/img/3d/icon1.webp'
import product2 from '@/assets/img/3d/icon2.webp'
import product3 from '@/assets/img/3d/icon3.webp'
import product4 from '@/assets/img/3d/icon4.webp'
import product5 from '@/assets/img/3d/icon5.webp'
import product6 from '@/assets/img/3d/icon6.webp'
const tableData = reactive([
{
name: '智能手表 Pro',
category: '电子设备',
price: 1299,
stock: 156,
sales: 423,
percentage: 75,
pro: 0,
color: 'rgb(var(--art-primary)) !important',
image: product1
},
{
name: '无线蓝牙耳机',
category: '音频设备',
price: 499,
stock: 89,
sales: 652,
percentage: 85,
pro: 0,
color: 'rgb(var(--art-success)) !important',
image: product2
},
{
name: '机械键盘',
category: '电脑配件',
price: 399,
stock: 12,
sales: 238,
percentage: 45,
pro: 0,
color: 'rgb(var(--art-warning)) !important',
image: product3
},
{
name: '超薄笔记本电脑',
category: '电子设备',
price: 5999,
stock: 0,
sales: 126,
percentage: 30,
pro: 0,
color: 'rgb(var(--art-error)) !important',
image: product4
},
{
name: '智能音箱',
category: '智能家居',
price: 799,
stock: 45,
sales: 321,
percentage: 60,
pro: 0,
color: 'rgb(var(--art-info)) !important',
image: product5
},
{
name: '游戏手柄',
category: '游戏配件',
price: 299,
stock: 78,
sales: 489,
percentage: 70,
pro: 0,
color: 'rgb(var(--art-secondary)) !important',
image: product6
}
])
// 根据库存获取状态文本
const getStockStatus = (stock: number) => {
if (stock === 0) return '缺货'
if (stock < 20) return '低库存'
if (stock < 50) return '适中'
return '充足'
}
// 根据库存获取状态类名
const getStockClass = (stock: number) => {
if (stock === 0) return 'out-of-stock'
if (stock < 20) return 'low-stock'
if (stock < 50) return 'medium-stock'
return 'in-stock'
}
onMounted(() => {
addAnimation()
})
const addAnimation = () => {
setTimeout(() => {
for (let i = 0; i < tableData.length; i++) {
let item = tableData[i]
tableData[i].pro = item.percentage
}
}, 100)
}
</script>
<style lang="scss" scoped>
.table {
width: 100%;
.card-header {
padding-left: 25px !important;
}
.product-image {
width: 50px;
height: 50px;
object-fit: cover;
border-radius: 6px;
}
.product-info {
display: flex;
flex-direction: column;
margin-left: 12px;
}
.product-name {
font-weight: 500;
}
.product-category {
font-size: 12px;
color: #64748b;
}
.price {
font-weight: 600;
}
.stock-badge {
display: inline-block;
padding: 4px 8px;
font-size: 12px;
font-weight: 500;
border-radius: 4px;
}
.in-stock {
color: rgb(var(--art-success));
background-color: rgba(var(--art-success-rgb), 0.1);
}
.medium-stock {
color: rgb(var(--art-info));
background-color: rgba(var(--art-info-rgb), 0.1);
}
.low-stock {
color: rgb(var(--art-warning));
background-color: rgba(var(--art-warning-rgb), 0.1);
}
.out-of-stock {
color: rgb(var(--art-error));
background-color: rgba(var(--art-error-rgb), 0.1);
}
}
</style>

View File

@@ -879,8 +879,8 @@
color: var(--el-text-color-regular); color: var(--el-text-color-regular);
em { em {
color: var(--el-color-primary);
font-style: normal; font-style: normal;
color: var(--el-color-primary);
} }
} }
} }

View File

@@ -62,7 +62,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { h, onMounted, ref, computed } from 'vue' import { h, onMounted, ref, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ElButton, ElCard, ElIcon, ElImage, ElMessage, ElTable, ElTableColumn } from 'element-plus' import {
ElButton,
ElCard,
ElIcon,
ElImage,
ElMessage,
ElTable,
ElTableColumn
} from 'element-plus'
import { ArrowLeft, Loading } from '@element-plus/icons-vue' import { ArrowLeft, Loading } from '@element-plus/icons-vue'
import DetailPage from '@/components/common/DetailPage.vue' import DetailPage from '@/components/common/DetailPage.vue'
import type { DetailSection } from '@/components/common/DetailPage.vue' import type { DetailSection } from '@/components/common/DetailPage.vue'
@@ -207,8 +215,7 @@
baseFields.push({ baseFields.push({
label: '实付金额', label: '实付金额',
prop: 'actual_paid_amount', prop: 'actual_paid_amount',
formatter: (value) => formatter: (value) => (value !== undefined && value !== null ? formatCurrency(value) : '-')
value !== undefined && value !== null ? formatCurrency(value) : '-'
}) })
} }
@@ -254,8 +261,7 @@
}, },
{ {
label: '订单渠道', label: '订单渠道',
formatter: (_, data) => formatter: (_, data) => (data.purchase_role ? getPurchaseRoleText(data.purchase_role) : '-')
data.purchase_role ? getPurchaseRoleText(data.purchase_role) : '-'
}, },
{ {
label: '购买备注', label: '购买备注',

View File

@@ -152,17 +152,17 @@
</ElOption> </ElOption>
</ElSelect> </ElSelect>
<template v-if="noSeriesId"> <template v-if="noSeriesId">
<div style="color: var(--el-text-color-warning); font-size: 12px; margin-top: 4px"> <div style=" margin-top: 4px; font-size: 12px;color: var(--el-text-color-warning)">
该设备未关联套餐系列无法购买套餐 该设备未关联套餐系列无法购买套餐
</div> </div>
</template> </template>
<template v-else-if="packagesEmptyWithSeriesId"> <template v-else-if="packagesEmptyWithSeriesId">
<div style="color: var(--el-text-color-info); font-size: 12px; margin-top: 4px"> <div style=" margin-top: 4px; font-size: 12px;color: var(--el-text-color-info)">
该系列暂无可用套餐 该系列暂无可用套餐
</div> </div>
</template> </template>
<template v-else-if="selectedPackageIsGift"> <template v-else-if="selectedPackageIsGift">
<div style="color: var(--el-color-warning); font-size: 12px; margin-top: 4px"> <div style=" margin-top: 4px; font-size: 12px;color: var(--el-color-warning)">
提示: 赠送套餐只能使用线下支付方式且必须上传支付凭证 提示: 赠送套餐只能使用线下支付方式且必须上传支付凭证
</div> </div>
</template> </template>
@@ -1403,8 +1403,8 @@
color: var(--el-text-color-regular); color: var(--el-text-color-regular);
em { em {
color: var(--el-color-primary);
font-style: normal; font-style: normal;
color: var(--el-color-primary);
} }
} }
} }

View File

@@ -1542,25 +1542,25 @@
.gift-switch-wrapper { .gift-switch-wrapper {
display: flex; display: flex;
align-items: center;
gap: 12px; gap: 12px;
align-items: center;
} }
.gift-hint { .gift-hint {
display: flex; display: flex;
align-items: center;
gap: 6px; gap: 6px;
align-items: center;
padding: 6px 10px; padding: 6px 10px;
border-radius: 4px;
font-size: 12px; font-size: 12px;
line-height: 1.4; line-height: 1.4;
color: var(--el-color-warning);
white-space: nowrap; white-space: nowrap;
background-color: var(--el-color-warning-light-9); background-color: var(--el-color-warning-light-9);
color: var(--el-color-warning); border-radius: 4px;
&--info { &--info {
background-color: var(--el-fill-color-light);
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
background-color: var(--el-fill-color-light);
} }
.gift-hint-icon { .gift-hint-icon {

View File

@@ -113,7 +113,7 @@
style="width: 100%" style="width: 100%"
/> />
<span <span
style="color: var(--el-text-color-secondary); font-size: 12px; margin-left: 8px" style=" margin-left: 8px; font-size: 12px;color: var(--el-text-color-secondary)"
>单位</span >单位</span
> >
</ElFormItem> </ElFormItem>
@@ -138,7 +138,7 @@
style="width: 100%" style="width: 100%"
/> />
<span <span
style="color: var(--el-text-color-secondary); font-size: 12px; margin-left: 8px" style=" margin-left: 8px; font-size: 12px;color: var(--el-text-color-secondary)"
>单位</span >单位</span
> >
</ElFormItem> </ElFormItem>
@@ -156,7 +156,7 @@
> >
<ElCard shadow="hover"> <ElCard shadow="hover">
<div style="display: flex; gap: 12px; align-items: flex-start"> <div style="display: flex; gap: 12px; align-items: flex-start">
<div style="flex: 1; display: flex; flex-direction: column; gap: 12px"> <div style=" display: flex;flex: 1; flex-direction: column; gap: 12px">
<!-- 第一行比较运算符和阈值 --> <!-- 第一行比较运算符和阈值 -->
<div style="display: flex; gap: 12px"> <div style="display: flex; gap: 12px">
<div style="flex: 1"> <div style="flex: 1">
@@ -269,10 +269,10 @@
@click="removeTier(index)" @click="removeTier(index)"
style=" style="
flex-shrink: 0; flex-shrink: 0;
margin-top: 28px;
width: 32px; width: 32px;
height: 32px; height: 32px;
padding: 0; padding: 0;
margin-top: 28px;
" "
/> />
</div> </div>
@@ -308,7 +308,7 @@
style="width: 100%" style="width: 100%"
/> />
<span <span
style="color: var(--el-text-color-secondary); font-size: 12px; margin-left: 8px" style=" margin-left: 8px; font-size: 12px;color: var(--el-text-color-secondary)"
>单位</span >单位</span
> >
</ElFormItem> </ElFormItem>
@@ -362,7 +362,7 @@
style="width: 100%" style="width: 100%"
/> />
<span <span
style="color: var(--el-text-color-secondary); font-size: 12px; margin-left: 8px" style=" margin-left: 8px; font-size: 12px;color: var(--el-text-color-secondary)"
>单位</span >单位</span
> >
</ElFormItem> </ElFormItem>

View File

@@ -1147,7 +1147,7 @@
if (suggestedRetailPrice === undefined || suggestedRetailPrice === null) { if (suggestedRetailPrice === undefined || suggestedRetailPrice === null) {
return '-' return '-'
} }
return `¥${(suggestedRetailPrice * 1.5).toFixed(2)}` return `¥${suggestedRetailPrice.toFixed(2)}`
} }
// 监听套餐选择变化 // 监听套餐选择变化

View File

@@ -439,13 +439,13 @@
.header-left { .header-left {
display: flex; display: flex;
align-items: center;
gap: 16px; gap: 16px;
align-items: center;
} }
.grant-name { .grant-name {
font-weight: 600;
font-size: 16px; font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary); color: var(--el-text-color-primary);
} }
} }
@@ -456,9 +456,9 @@
} }
.form-tip { .form-tip {
margin-top: 4px;
font-size: 12px; font-size: 12px;
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
margin-top: 4px;
} }
.dialog-footer { .dialog-footer {

View File

@@ -60,14 +60,14 @@
<template #default> <template #default>
<el-table-column v-for="col in columns" :key="col.prop || col.type" v-bind="col"> <el-table-column v-for="col in columns" :key="col.prop || col.type" v-bind="col">
<template v-if="col.slots?.default === 'progress'" #default="scope"> <template v-if="col.slots?.default === 'progress'" #default="scope">
<div style="display: flex; align-items: center; gap: 8px"> <div style="display: flex; gap: 8px; align-items: center">
<el-progress <el-progress
:percentage="getProgressPercentage(scope.row)" :percentage="getProgressPercentage(scope.row)"
stroke-width="6" stroke-width="6"
:show-text="false" :show-text="false"
style="flex: 1" style="flex: 1"
/> />
<span style="font-size: 12px; width: 50px" <span style=" width: 50px;font-size: 12px"
>{{ getProgressPercentage(scope.row) }}%</span >{{ getProgressPercentage(scope.row) }}%</span
> >
</div> </div>
@@ -103,7 +103,7 @@
label="任务类型" label="任务类型"
prop="task_type" prop="task_type"
label-width="80" label-width="80"
style="margin-bottom: 0; width: 300px" style=" width: 300px;margin-bottom: 0"
> >
<el-select <el-select
v-model="batchForm.task_type" v-model="batchForm.task_type"

View File

@@ -585,11 +585,15 @@
trigger: 'blur' trigger: 'blur'
} }
], ],
wx_serial_no: [{ validator: requiredValidator(isWechatProvider, '证书序列号'), trigger: 'blur' }], wx_serial_no: [
{ validator: requiredValidator(isWechatProvider, '证书序列号'), trigger: 'blur' }
],
wx_cert_content: [ wx_cert_content: [
{ validator: requiredValidator(isWechatProvider, '支付证书内容'), trigger: 'blur' } { validator: requiredValidator(isWechatProvider, '支付证书内容'), trigger: 'blur' }
], ],
wx_key_content: [{ validator: requiredValidator(isWechatProvider, '支付密钥内容'), trigger: 'blur' }], wx_key_content: [
{ validator: requiredValidator(isWechatProvider, '支付密钥内容'), trigger: 'blur' }
],
oa_oauth_redirect_url: [{ validator: urlValidator, trigger: 'blur' }], oa_oauth_redirect_url: [{ validator: urlValidator, trigger: 'blur' }],
wx_notify_url: [{ validator: urlValidator, trigger: 'blur' }], wx_notify_url: [{ validator: urlValidator, trigger: 'blur' }],
ali_notify_url: [{ validator: urlValidator, trigger: 'blur' }], ali_notify_url: [{ validator: urlValidator, trigger: 'blur' }],
@@ -704,7 +708,9 @@
}) })
} }
return h(ElTag, { type: row.is_active ? 'success' : 'info' }, () => getStatusText(row.is_active)) return h(ElTag, { type: row.is_active ? 'success' : 'info' }, () =>
getStatusText(row.is_active)
)
} }
}, },
{ {

View File

@@ -1108,8 +1108,8 @@
.role-info { .role-info {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
justify-content: space-between;
.role-name { .role-name {
font-size: 14px; font-size: 14px;

View File

@@ -1,771 +0,0 @@
<template>
<div class="chat" :style="{ height: containerMinHeight }">
<ElRow>
<ElCol :span="12">
<div class="grid-content ep-bg-purple" />
</ElCol>
<ElCol :span="12">
<div class="grid-content ep-bg-purple-light" />
</ElCol>
</ElRow>
<div class="person-list">
<div class="person-item-header">
<div class="user-info">
<ElAvatar :size="50" :src="selectedPerson?.avatar" />
<div class="user-details">
<div class="name">{{ selectedPerson?.name }}</div>
<div class="email">{{ selectedPerson?.email }}</div>
</div>
</div>
<div class="search-box">
<ElInput v-model="searchQuery" placeholder="搜索联系人" prefix-icon="Search" clearable />
</div>
<ElDropdown trigger="click" placement="bottom-start">
<span class="sort-btn">
排序方式
<ElIcon class="el-icon--right">
<arrow-down />
</ElIcon>
</span>
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem>按时间排序</ElDropdownItem>
<ElDropdownItem>按名称排序</ElDropdownItem>
<ElDropdownItem>全部标为已读</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
</div>
<ElScrollbar>
<div
v-for="item in personList"
:key="item.id"
class="person-item"
:class="{ active: selectedPerson?.id === item.id }"
@click="selectPerson(item)"
>
<div class="avatar-wrapper">
<ElAvatar :size="40" :src="item.avatar">
{{ item.name.charAt(0) }}
</ElAvatar>
<div class="status-dot" :class="{ online: item.online }"></div>
</div>
<div class="person-info">
<div class="info-top">
<span class="person-name">{{ item.name }}</span>
<span class="last-time">{{ item.lastTime }}</span>
</div>
<div class="info-bottom">
<span class="email">{{ item.email }}</span>
</div>
</div>
</div>
</ElScrollbar>
</div>
<div class="chat-modal">
<div class="header">
<div class="header-left">
<span class="name">Art Bot</span>
<div class="status">
<div class="dot" :class="{ online: isOnline, offline: !isOnline }"></div>
<span class="status-text">{{ isOnline ? '在线' : '离线' }}</span>
</div>
</div>
<div class="header-right">
<div class="btn">
<i class="iconfont-sys">&#xe776;</i>
</div>
<div class="btn">
<i class="iconfont-sys">&#xe778;</i>
</div>
<div class="btn">
<i class="iconfont-sys">&#xe6df;</i>
</div>
</div>
</div>
<div class="chat-container">
<!-- 聊天消息区域 -->
<div class="chat-messages" ref="messageContainer">
<template v-for="(message, index) in messages" :key="index">
<div :class="['message-item', message.isMe ? 'message-right' : 'message-left']">
<ElAvatar :size="32" :src="message.avatar" class="message-avatar" />
<div class="message-content">
<div class="message-info">
<span class="sender-name">{{ message.sender }}</span>
<span class="message-time">{{ message.time }}</span>
</div>
<div class="message-text">{{ message.content }}</div>
</div>
</div>
</template>
</div>
<!-- 聊天输入区域 -->
<div class="chat-input">
<ElInput
v-model="messageText"
type="textarea"
:rows="3"
placeholder="输入消息"
resize="none"
@keyup.enter.prevent="sendMessage"
>
<template #append>
<div class="input-actions">
<ElButton :icon="Paperclip" circle plain />
<ElButton :icon="Picture" circle plain />
<ElButton type="primary" @click="sendMessage" v-ripple>发送</ElButton>
</div>
</template>
</ElInput>
<div class="chat-input-actions">
<div class="left">
<i class="iconfont-sys">&#xe634;</i>
<i class="iconfont-sys">&#xe809;</i>
</div>
<ElButton type="primary" @click="sendMessage" v-ripple>发送</ElButton>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { Picture, Paperclip } from '@element-plus/icons-vue'
import { mittBus } from '@/utils/sys'
import meAvatar from '@/assets/img/avatar/avatar5.webp'
import aiAvatar from '@/assets/img/avatar/avatar10.webp'
import avatar2 from '@/assets/img/avatar/avatar2.webp'
import avatar3 from '@/assets/img/avatar/avatar3.webp'
import avatar4 from '@/assets/img/avatar/avatar4.webp'
import avatar5 from '@/assets/img/avatar/avatar5.webp'
import avatar6 from '@/assets/img/avatar/avatar6.webp'
import avatar7 from '@/assets/img/avatar/avatar7.webp'
import avatar8 from '@/assets/img/avatar/avatar8.webp'
import avatar9 from '@/assets/img/avatar/avatar9.webp'
import avatar10 from '@/assets/img/avatar/avatar10.webp'
import { useCommon } from '@/composables/useCommon'
const { containerMinHeight } = useCommon()
const searchQuery = ref('')
// 抽屉显示状态
const isDrawerVisible = ref(false)
// 是否在线
const isOnline = ref(true)
interface Person {
id: number
name: string
email: string
avatar: string
online?: boolean
lastTime: string
unread?: number
}
const selectedPerson = ref<Person | null>(null)
const personList = ref<Person[]>([
{
id: 1,
name: '梅洛迪·梅西',
email: 'melody@altbox.com',
avatar: meAvatar,
online: true,
lastTime: '20小时前',
unread: 0
},
{
id: 2,
name: '马克·史密斯',
email: 'max@kt.com',
avatar: avatar2,
online: true,
lastTime: '2周前',
unread: 6
},
{
id: 3,
name: '肖恩·宾',
email: 'sean@dellito.com',
avatar: avatar3,
online: false,
lastTime: '5小时前',
unread: 5
},
{
id: 4,
name: '爱丽丝·约翰逊',
email: 'alice@domain.com',
avatar: avatar4,
online: true,
lastTime: '1小时前',
unread: 2
},
{
id: 5,
name: '鲍勃·布朗',
email: 'bob@domain.com',
avatar: avatar5,
online: false,
lastTime: '3天前',
unread: 1
},
{
id: 6,
name: '查理·戴维斯',
email: 'charlie@domain.com',
avatar: avatar6,
online: true,
lastTime: '10分钟前',
unread: 0
},
{
id: 7,
name: '戴安娜·普林斯',
email: 'diana@domain.com',
avatar: avatar7,
online: true,
lastTime: '15分钟前',
unread: 3
},
{
id: 8,
name: '伊桑·亨特',
email: 'ethan@domain.com',
avatar: avatar8,
online: true,
lastTime: '5分钟前',
unread: 0
},
{
id: 9,
name: '杰西卡·琼斯',
email: 'jessica@domain.com',
avatar: avatar9,
online: false,
lastTime: '1天前',
unread: 4
},
{
id: 10,
name: '彼得·帕克',
email: 'peter@domain.com',
avatar: avatar10,
online: true,
lastTime: '2小时前',
unread: 1
},
{
id: 11,
name: '克拉克·肯特',
email: 'clark@domain.com',
avatar: avatar3,
online: true,
lastTime: '30分钟前',
unread: 2
},
{
id: 12,
name: '布鲁斯·韦恩',
email: 'bruce@domain.com',
avatar: avatar5,
online: false,
lastTime: '3天前',
unread: 0
},
{
id: 13,
name: '韦德·威尔逊',
email: 'wade@domain.com',
avatar: avatar6,
online: true,
lastTime: '10分钟前',
unread: 5
}
])
const selectPerson = (person: Person) => {
selectedPerson.value = person
}
// 消息相关数据
const messageText = ref('')
const messages = ref([
{
id: 1,
sender: 'Art Bot',
content: '你好我是你的AI助手有什么我可以帮你的吗',
time: '10:00',
isMe: false,
avatar: aiAvatar
},
{
id: 2,
sender: 'Ricky',
content: '我想了解一下系统的使用方法。',
time: '10:01',
isMe: true,
avatar: meAvatar
},
{
id: 3,
sender: 'Art Bot',
content: '好的,我来为您介绍系统的主要功能。首先,您可以通过左侧菜单访问不同的功能模块...',
time: '10:02',
isMe: false,
avatar: aiAvatar
},
{
id: 4,
sender: 'Ricky',
content: '听起来很不错,能具体讲讲数据分析部分吗?',
time: '10:05',
isMe: true,
avatar: meAvatar
},
{
id: 5,
sender: 'Art Bot',
content: '当然可以。数据分析模块可以帮助您实时监控关键指标,并生成详细的报表...',
time: '10:06',
isMe: false,
avatar: aiAvatar
},
{
id: 6,
sender: 'Ricky',
content: '太好了,那我如何开始使用呢?',
time: '10:08',
isMe: true,
avatar: meAvatar
},
{
id: 7,
sender: 'Art Bot',
content: '您可以先创建一个项目,然后在项目中添加相关的数据源,系统会自动进行分析。',
time: '10:09',
isMe: false,
avatar: aiAvatar
},
{
id: 8,
sender: 'Ricky',
content: '明白了,谢谢你的帮助!',
time: '10:10',
isMe: true,
avatar: meAvatar
},
{
id: 9,
sender: 'Art Bot',
content: '不客气,有任何问题随时联系我。',
time: '10:11',
isMe: false,
avatar: aiAvatar
}
])
const messageId = ref(10) // 用于生成唯一的消息ID
const userAvatar = ref(meAvatar) // 使用导入的头像
// 发送消息
const sendMessage = () => {
const text = messageText.value.trim()
if (!text) return
messages.value.push({
id: messageId.value++,
sender: 'Ricky',
content: text,
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
isMe: true,
avatar: userAvatar.value
})
messageText.value = ''
scrollToBottom()
}
// 滚动到底部
const messageContainer = ref<HTMLElement | null>(null)
const scrollToBottom = () => {
setTimeout(() => {
if (messageContainer.value) {
messageContainer.value.scrollTop = messageContainer.value.scrollHeight
}
}, 100)
}
const openChat = () => {
isDrawerVisible.value = true
}
onMounted(() => {
scrollToBottom()
mittBus.on('openChat', openChat)
selectedPerson.value = personList.value[0]
})
</script>
<style lang="scss">
.chat-modal {
.el-overlay {
background-color: rgb(0 0 0 / 20%) !important;
}
}
</style>
<style lang="scss" scoped>
.chat {
display: flex;
overflow: hidden;
background-color: var(--art-main-bg-color);
border: 1px solid var(--art-border-color);
border-radius: 10px;
.person-list {
box-sizing: border-box;
width: 360px;
height: 100%;
padding: 20px;
border-right: 1px solid var(--art-border-color);
.person-item-header {
padding-bottom: 20px;
.user-info {
display: flex;
gap: 12px;
align-items: center;
.user-details {
.name {
font-size: 16px;
font-weight: 500;
color: var(--art-gray-900);
}
.email {
margin-top: 4px;
font-size: 13px;
color: var(--art-gray-500);
}
}
}
.search-box {
margin-top: 12px;
}
.sort-btn {
margin-top: 20px;
cursor: pointer;
}
}
.person-item {
display: flex;
align-items: center;
padding: 12px;
cursor: pointer;
border-radius: 8px;
transition: all 0.3s ease;
&:hover,
&.active {
background-color: var(--el-fill-color-light);
}
.avatar-wrapper {
position: relative;
margin-right: 12px;
.status-dot {
position: absolute;
right: 1px;
bottom: 1px;
width: 9px;
height: 9px;
background-color: var(--el-color-error);
border-radius: 50%;
&.online {
background-color: var(--el-color-success);
}
}
}
.person-info {
flex: 1;
min-width: 0;
.info-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 4px;
.person-name {
font-size: 14px;
font-weight: 500;
color: var(--el-text-color-primary);
}
.last-time {
font-size: 12px;
color: var(--el-text-color-secondary);
}
}
.info-bottom {
display: flex;
align-items: center;
justify-content: space-between;
.email {
overflow: hidden;
font-size: 12px;
color: var(--el-text-color-secondary);
text-overflow: ellipsis;
white-space: nowrap;
}
.unread-badge {
:deep(.el-badge__content) {
border: none;
}
}
}
}
}
}
.chat-modal {
box-sizing: border-box;
flex: 1;
height: 100%;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 16px 0;
margin-bottom: 20px;
.header-left {
.name {
font-size: 16px;
font-weight: 500;
}
.status {
display: flex;
gap: 4px;
align-items: center;
margin-top: 6px;
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
&.online {
background-color: var(--el-color-success);
}
&.offline {
background-color: var(--el-color-danger);
}
}
.status-text {
font-size: 12px;
color: var(--art-gray-600);
}
}
}
.header-right {
display: flex;
gap: 8px;
align-items: center;
.btn {
width: 42px;
height: 42px;
line-height: 42px;
text-align: center;
cursor: pointer;
border-radius: 50%;
transition: background-color 0.2s ease;
&:hover {
background-color: var(--art-gray-200);
}
i {
font-size: 20px;
color: var(--art-text-gray-700);
}
}
}
}
.chat-container {
display: flex;
flex-direction: column;
height: calc(100% - 85px);
.chat-messages {
flex: 1;
padding: 30px 16px;
overflow-y: auto;
border-top: 1px solid var(--el-border-color-lighter);
&::-webkit-scrollbar {
width: 5px !important;
}
.message-item {
display: flex;
flex-direction: row;
gap: 8px;
align-items: flex-start;
width: 100%;
margin-bottom: 30px;
.message-text {
font-size: 14px;
color: var(--art-gray-900);
border-radius: 6px;
}
&.message-left {
justify-content: flex-start;
.message-content {
align-items: flex-start;
.message-info {
flex-direction: row;
}
.message-text {
background-color: var(--art-gray-200);
}
}
}
&.message-right {
flex-direction: row-reverse;
.message-content {
align-items: flex-end;
.message-info {
flex-direction: row-reverse;
}
.message-text {
background-color: #e9f3ff;
background-color: rgb(var(--art-bg-secondary));
}
}
}
.message-avatar {
flex-shrink: 0;
}
.message-content {
display: flex;
flex-direction: column;
max-width: 70%;
.message-info {
display: flex;
gap: 8px;
margin-bottom: 4px;
font-size: 12px;
.message-time {
color: var(--el-text-color-secondary);
}
.sender-name {
font-weight: 500;
}
}
.message-text {
padding: 10px 14px;
line-height: 1.4;
}
}
}
}
.chat-input {
padding: 16px; // 增加填充以提升输入区域的布局
.input-actions {
display: flex;
gap: 8px;
padding: 8px 0;
}
.chat-input-actions {
display: flex;
align-items: center; // 修正为单数
justify-content: space-between;
margin-top: 12px;
.left {
display: flex;
align-items: center;
i {
margin-right: 20px;
font-size: 16px;
color: var(--art-gray-500);
cursor: pointer;
}
}
// 确保发送按钮与输入框对齐
el-button {
min-width: 80px;
}
}
}
}
}
@media only screen and (max-width: $device-ipad-pro) {
.chat {
flex-direction: column;
.person-list {
width: 100%;
height: 170px;
border-right: none;
.person-item-header {
display: none;
}
}
.chat-modal {
height: calc(70% - 30px);
}
}
}
</style>