Initial commit: One Pipe System

完整的管理系统,包含账户管理、卡片管理、套餐管理、财务管理等功能模块。

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
sexygoat
2026-01-22 16:35:33 +08:00
commit 222e5bb11a
495 changed files with 145440 additions and 0 deletions

54
src/App.vue Normal file
View File

@@ -0,0 +1,54 @@
<template>
<ElConfigProvider size="default" :locale="locales[language]" :z-index="3000">
<RouterView></RouterView>
</ElConfigProvider>
</template>
<script setup lang="ts">
import { useUserStore } from './store/modules/user'
import zh from 'element-plus/es/locale/lang/zh-cn'
import en from 'element-plus/es/locale/lang/en'
import { systemUpgrade } from '@/utils'
import { UserService } from './api/usersApi'
import { ApiStatus } from './utils/http/status'
import { setThemeTransitionClass } from '@/utils'
import { checkStorageCompatibility } from '@/utils'
const userStore = useUserStore()
const { language } = storeToRefs(userStore)
const locales = {
zh: zh,
en: en
}
onBeforeMount(() => {
setThemeTransitionClass(true)
})
onMounted(() => {
// 检查存储兼容性
checkStorageCompatibility()
// 提升暗黑主题下页面刷新视觉体验
setThemeTransitionClass(false)
// 系统升级
systemUpgrade()
// 获取用户信息
getUserInfo()
})
// 获取用户信息
const getUserInfo = async () => {
if (userStore.isLogin && userStore.accessToken) {
try {
const res = await UserService.getUserInfo()
if (res.code === ApiStatus.success && res.data) {
// API 返回的是 { user, permissions },我们需要保存 user
userStore.setUserInfo(res.data.user)
}
} catch (error) {
console.error('获取用户信息失败:', error)
}
}
}
</script>

218
src/api/BaseService.ts Normal file
View File

@@ -0,0 +1,218 @@
/**
* API 服务基类
* 提供统一的 HTTP 请求方法
*/
import request from '@/utils/http'
import type { BaseResponse, PaginationResponse, ListResponse } from '@/types/api'
export class BaseService {
/**
* GET 请求
* @param url 请求URL
* @param params 请求参数
* @param config 额外配置
*/
protected static get<T = any>(
url: string,
params?: Record<string, any>,
config?: Record<string, any>
): Promise<T> {
return request.get<T>({
url,
params,
...config
})
}
/**
* POST 请求
* @param url 请求URL
* @param data 请求数据
* @param config 额外配置
*/
protected static post<T = any>(
url: string,
data?: Record<string, any>,
config?: Record<string, any>
): Promise<T> {
return request.post<T>({
url,
data,
...config
})
}
/**
* PUT 请求
* @param url 请求URL
* @param data 请求数据
* @param config 额外配置
*/
protected static put<T = any>(
url: string,
data?: Record<string, any>,
config?: Record<string, any>
): Promise<T> {
return request.put<T>({
url,
data,
...config
})
}
/**
* DELETE 请求
* @param url 请求URL
* @param params 请求参数
* @param config 额外配置
*/
protected static delete<T = any>(
url: string,
params?: Record<string, any>,
config?: Record<string, any>
): Promise<T> {
return request.del<T>({
url,
params,
...config
})
}
/**
* 获取单个资源
* @param url 请求URL
* @param params 请求参数
*/
protected static getOne<T>(
url: string,
params?: Record<string, any>
): Promise<BaseResponse<T>> {
return this.get<BaseResponse<T>>(url, params)
}
/**
* 获取列表(不分页)
* @param url 请求URL
* @param params 请求参数
*/
protected static getList<T>(
url: string,
params?: Record<string, any>
): Promise<ListResponse<T>> {
return this.get<ListResponse<T>>(url, params)
}
/**
* 获取分页列表
* @param url 请求URL
* @param params 请求参数
*/
protected static getPage<T>(
url: string,
params?: Record<string, any>
): Promise<PaginationResponse<T>> {
return this.get<PaginationResponse<T>>(url, params)
}
/**
* 创建资源
* @param url 请求URL
* @param data 请求数据
*/
protected static create<T = any>(
url: string,
data: Record<string, any>
): Promise<BaseResponse<T>> {
return this.post<BaseResponse<T>>(url, data)
}
/**
* 更新资源
* @param url 请求URL
* @param data 请求数据
*/
protected static update<T = any>(
url: string,
data: Record<string, any>
): Promise<BaseResponse<T>> {
return this.put<BaseResponse<T>>(url, data)
}
/**
* 删除资源
* @param url 请求URL
* @param params 请求参数
*/
protected static remove<T = any>(
url: string,
params?: Record<string, any>
): Promise<BaseResponse<T>> {
return this.delete<BaseResponse<T>>(url, params)
}
/**
* 批量删除
* @param url 请求URL
* @param ids ID列表
*/
protected static batchDelete(url: string, ids: (string | number)[]): Promise<BaseResponse> {
return this.delete<BaseResponse>(url, { ids })
}
/**
* 上传文件
* @param url 请求URL
* @param file 文件
* @param params 额外参数
*/
protected static upload<T = any>(
url: string,
file: File,
params?: Record<string, any>
): Promise<BaseResponse<T>> {
const formData = new FormData()
formData.append('file', file)
if (params) {
Object.keys(params).forEach((key) => {
formData.append(key, params[key])
})
}
return request.post<BaseResponse<T>>({
url,
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
/**
* 下载文件
* @param url 请求URL
* @param params 请求参数
* @param fileName 文件名
*/
protected static download(
url: string,
params?: Record<string, any>,
fileName?: string
): Promise<void> {
return request.get({
url,
params,
responseType: 'blob'
}).then((blob: any) => {
const downloadUrl = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = downloadUrl
link.download = fileName || 'download'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(downloadUrl)
})
}
}

45
src/api/articleApi.ts Normal file
View File

@@ -0,0 +1,45 @@
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
})
}
}

48
src/api/authApi.ts Normal file
View File

@@ -0,0 +1,48 @@
/**
* 认证相关 API
*/
import request from '@/utils/http'
import { BaseResponse, LoginParams, LoginData, UserInfo, UserInfoResponse, RefreshTokenData } from '@/types/api'
export class AuthService {
/**
* 用户登录
* @param params 登录参数
*/
static login(params: LoginParams): Promise<BaseResponse<LoginData>> {
return request.post<BaseResponse<LoginData>>({
url: '/api/admin/login',
data: params
})
}
/**
* 获取用户信息
* GET /api/admin/me
*/
static getUserInfo(): Promise<BaseResponse<UserInfoResponse>> {
return request.get<BaseResponse<UserInfoResponse>>({
url: '/api/admin/me'
})
}
/**
* 用户登出
*/
static logout(): Promise<BaseResponse<void>> {
return request.post<BaseResponse<void>>({
url: '/api/admin/logout'
})
}
/**
* 刷新 Token
* @param refreshToken 刷新令牌
*/
static refreshToken(refreshToken: string): Promise<BaseResponse<RefreshTokenData>> {
return request.post<BaseResponse<RefreshTokenData>>({
url: '/api/auth/refresh',
data: { refreshToken }
})
}
}

25
src/api/menuApi.ts Normal file
View File

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

@@ -0,0 +1,97 @@
/**
* 账号相关 API - 匹配后端实际接口
*/
import { BaseService } from '../BaseService'
import type {
PlatformAccount,
AccountQueryParams,
CreatePlatformAccountParams,
BaseResponse,
PaginationResponse
} from '@/types/api'
export class AccountService extends BaseService {
// ========== 账号管理 (Account Management) ==========
/**
* 获取账号列表
* GET /api/admin/accounts
* @param params 查询参数
*/
static getAccounts(
params?: AccountQueryParams
): Promise<PaginationResponse<PlatformAccount>> {
return this.getPage<PlatformAccount>('/api/admin/accounts', params)
}
/**
* 创建账号
* POST /api/admin/accounts
* @param data 账号数据
*/
static createAccount(data: CreatePlatformAccountParams): Promise<BaseResponse> {
return this.create('/api/admin/accounts', data)
}
/**
* 更新账号
* PUT /api/admin/accounts/{id}
* @param id 账号ID
* @param data 账号数据
*/
static updateAccount(
id: number,
data: Partial<CreatePlatformAccountParams>
): Promise<BaseResponse> {
return this.update(`/api/admin/accounts/${id}`, data)
}
/**
* 删除账号
* DELETE /api/admin/accounts/{id}
* @param id 账号ID
*/
static deleteAccount(id: number): Promise<BaseResponse> {
return this.remove(`/api/admin/accounts/${id}`)
}
/**
* 获取账号详情
* GET /api/admin/accounts/{id}
* @param id 账号ID
*/
static getAccountDetail(id: number): Promise<BaseResponse<PlatformAccount>> {
return this.getOne<PlatformAccount>(`/api/admin/accounts/${id}`)
}
/**
* 获取账号的角色列表
* GET /api/admin/accounts/{id}/roles
* @param id 账号ID
* @returns 返回角色对象数组
*/
static getAccountRoles(id: number): Promise<BaseResponse<any[]>> {
return this.get<BaseResponse<any[]>>(`/api/admin/accounts/${id}/roles`)
}
/**
* 为账号分配角色
* POST /api/admin/accounts/{id}/roles
* @param id 账号ID
* @param roleIds 角色ID列表
*/
static assignRolesToAccount(id: number, roleIds: number[]): Promise<BaseResponse> {
return this.post<BaseResponse>(`/api/admin/accounts/${id}/roles`, { role_ids: roleIds })
}
/**
* 移除账号的单个角色
* DELETE /api/admin/accounts/{account_id}/roles/{role_id}
* @param accountId 账号ID
* @param roleId 角色ID
*/
static removeRoleFromAccount(accountId: number, roleId: number): Promise<BaseResponse> {
return this.delete<BaseResponse>(`/api/admin/accounts/${accountId}/roles/${roleId}`)
}
}

View File

@@ -0,0 +1,72 @@
/**
* 文章相关类型定义
*/
// 文章类型 (新命名规范)
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
}

63
src/api/modules/auth.ts Normal file
View File

@@ -0,0 +1,63 @@
/**
* 认证相关 API
*/
import { BaseService } from '../BaseService'
import type {
LoginParams,
LoginData,
UserInfo,
UserInfoResponse,
RefreshTokenParams,
RefreshTokenData,
ChangePasswordParams,
BaseResponse
} from '@/types/api'
export class AuthService extends BaseService {
/**
* 用户登录
* @param params 登录参数
*/
static login(params: LoginParams): Promise<BaseResponse<LoginData>> {
return this.post<BaseResponse<LoginData>>('/api/admin/login', params)
}
/**
* 退出登录
*/
static logout(): Promise<BaseResponse> {
return this.post<BaseResponse>('/api/admin/logout')
}
/**
* 获取当前用户信息
* GET /api/admin/me
*/
static getUserInfo(): Promise<BaseResponse<UserInfoResponse>> {
return this.get<BaseResponse<UserInfoResponse>>('/api/admin/me')
}
/**
* 刷新 Token
* @param params 刷新参数
*/
static refreshToken(params: RefreshTokenParams): Promise<BaseResponse<RefreshTokenData>> {
return this.post<BaseResponse<RefreshTokenData>>('/api/auth/refresh', params)
}
/**
* 修改密码
* @param params 修改密码参数
*/
static changePassword(params: ChangePasswordParams): Promise<BaseResponse> {
return this.post<BaseResponse>('/api/auth/change-password', params)
}
/**
* 获取验证码(如果需要)
*/
static getCaptcha(): Promise<BaseResponse<{ captchaId: string; captchaImage: string }>> {
return this.get<BaseResponse<{ captchaId: string; captchaImage: string }>>('/api/auth/captcha')
}
}

274
src/api/modules/card.ts Normal file
View File

@@ -0,0 +1,274 @@
/**
* 网卡相关 API
*/
import { BaseService } from '../BaseService'
import type {
Card,
SimCardProduct,
CardQueryParams,
CardImportBatch,
CardOperationParams,
CardAssignParams,
BatchRechargeRecord,
CardChangeApplication,
ProcessCardChangeParams,
FlowDetail,
SuspendResumeRecord,
CardOrder,
BaseResponse,
PaginationResponse,
ListResponse
} from '@/types/api'
export class CardService extends BaseService {
// ========== 号卡商品管理 ==========
/**
* 获取号卡商品列表
* @param params 查询参数
*/
static getSimCardProducts(params?: any): Promise<PaginationResponse<SimCardProduct>> {
return this.getPage<SimCardProduct>('/api/simcard-products', params)
}
/**
* 创建号卡商品
* @param data 商品数据
*/
static createSimCardProduct(data: Partial<SimCardProduct>): Promise<BaseResponse> {
return this.create('/api/simcard-products', data)
}
/**
* 更新号卡商品
* @param id 商品ID
* @param data 商品数据
*/
static updateSimCardProduct(
id: string | number,
data: Partial<SimCardProduct>
): Promise<BaseResponse> {
return this.update(`/api/simcard-products/${id}`, data)
}
/**
* 删除号卡商品
* @param id 商品ID
*/
static deleteSimCardProduct(id: string | number): Promise<BaseResponse> {
return this.remove(`/api/simcard-products/${id}`)
}
/**
* 号卡分配
* @param params 分配参数
*/
static assignCard(params: CardAssignParams): Promise<BaseResponse> {
return this.post<BaseResponse>('/api/simcard-products/assign', params)
}
// ========== 网卡管理 ==========
/**
* 获取网卡列表
* @param params 查询参数
*/
static getCards(params?: CardQueryParams): Promise<PaginationResponse<Card>> {
return this.getPage<Card>('/api/cards', params)
}
/**
* 根据ICCID获取单卡信息
* @param iccid ICCID
*/
static getCardByIccid(iccid: string): Promise<BaseResponse<Card>> {
return this.getOne<Card>(`/api/cards/iccid/${iccid}`)
}
/**
* 网卡操作(充值、停复机、增减流量等)
* @param params 操作参数
*/
static cardOperation(params: CardOperationParams): Promise<BaseResponse> {
return this.post<BaseResponse>('/api/cards/operation', params)
}
/**
* 套餐充值
* @param iccid ICCID
* @param packageId 套餐ID
*/
static rechargePackage(iccid: string, packageId: string | number): Promise<BaseResponse> {
return this.post<BaseResponse>(`/api/cards/${iccid}/recharge`, { packageId })
}
/**
* 停机
* @param iccid ICCID
* @param remark 备注
*/
static suspend(iccid: string, remark?: string): Promise<BaseResponse> {
return this.post<BaseResponse>(`/api/cards/${iccid}/suspend`, { remark })
}
/**
* 复机
* @param iccid ICCID
* @param remark 备注
*/
static resume(iccid: string, remark?: string): Promise<BaseResponse> {
return this.post<BaseResponse>(`/api/cards/${iccid}/resume`, { remark })
}
/**
* 获取流量详情
* @param iccid ICCID
* @param startDate 开始日期
* @param endDate 结束日期
*/
static getFlowDetails(
iccid: string,
startDate?: string,
endDate?: string
): Promise<ListResponse<FlowDetail>> {
return this.getList<FlowDetail>(`/api/cards/${iccid}/flow-details`, {
startDate,
endDate
})
}
/**
* 获取停复机记录
* @param iccid ICCID
*/
static getSuspendResumeRecords(iccid: string): Promise<ListResponse<SuspendResumeRecord>> {
return this.getList<SuspendResumeRecord>(`/api/cards/${iccid}/suspend-resume-records`)
}
/**
* 获取往期订单
* @param iccid ICCID
*/
static getCardOrders(iccid: string): Promise<ListResponse<CardOrder>> {
return this.getList<CardOrder>(`/api/cards/${iccid}/orders`)
}
/**
* 更改过期时间
* @param iccid ICCID
* @param expireTime 过期时间
*/
static changeExpireTime(iccid: string, expireTime: string): Promise<BaseResponse> {
return this.put<BaseResponse>(`/api/cards/${iccid}/expire-time`, { expireTime })
}
/**
* 增加流量
* @param iccid ICCID
* @param flow 流量MB
*/
static addFlow(iccid: string, flow: number): Promise<BaseResponse> {
return this.post<BaseResponse>(`/api/cards/${iccid}/add-flow`, { flow })
}
/**
* 减少流量
* @param iccid ICCID
* @param flow 流量MB
*/
static reduceFlow(iccid: string, flow: number): Promise<BaseResponse> {
return this.post<BaseResponse>(`/api/cards/${iccid}/reduce-flow`, { flow })
}
/**
* 变更钱包余额
* @param iccid ICCID
* @param amount 金额
*/
static changeWalletBalance(iccid: string, amount: number): Promise<BaseResponse> {
return this.put<BaseResponse>(`/api/cards/${iccid}/wallet`, { amount })
}
// ========== 批量操作 ==========
/**
* 获取导入批次列表
* @param params 查询参数
*/
static getImportBatches(params?: any): Promise<PaginationResponse<CardImportBatch>> {
return this.getPage<CardImportBatch>('/api/cards/import-batches', params)
}
/**
* 批量导入网卡
* @param file Excel文件
* @param params 额外参数
*/
static importCards(file: File, params?: Record<string, any>): Promise<BaseResponse> {
return this.upload('/api/cards/import', file, params)
}
/**
* 获取导入失败记录
* @param batchId 批次ID
*/
static getImportFailures(batchId: string | number): Promise<ListResponse<any>> {
return this.getList(`/api/cards/import-batches/${batchId}/failures`)
}
/**
* 批量充值记录列表
* @param params 查询参数
*/
static getBatchRechargeRecords(
params?: any
): Promise<PaginationResponse<BatchRechargeRecord>> {
return this.getPage<BatchRechargeRecord>('/api/cards/batch-recharge-records', params)
}
/**
* 批量充值导入
* @param file Excel文件
*/
static batchRecharge(file: File): Promise<BaseResponse> {
return this.upload('/api/cards/batch-recharge', file)
}
// ========== 换卡管理 ==========
/**
* 获取换卡申请列表
* @param params 查询参数
*/
static getCardChangeApplications(
params?: any
): Promise<PaginationResponse<CardChangeApplication>> {
return this.getPage<CardChangeApplication>('/api/card-change-applications', params)
}
/**
* 处理换卡申请
* @param params 处理参数
*/
static processCardChange(params: ProcessCardChangeParams): Promise<BaseResponse> {
return this.post<BaseResponse>('/api/card-change-applications/process', params)
}
/**
* 创建换卡通知
* @param iccids ICCID列表
* @param reason 换卡原因
*/
static createCardChangeNotice(iccids: string[], reason: string): Promise<BaseResponse> {
return this.post<BaseResponse>('/api/card-change-notices', { iccids, reason })
}
/**
* 获取换卡通知记录
* @param params 查询参数
*/
static getCardChangeNotices(params?: any): Promise<PaginationResponse<any>> {
return this.getPage('/api/card-change-notices', params)
}
}

22
src/api/modules/index.ts Normal file
View File

@@ -0,0 +1,22 @@
/**
* API 服务模块统一导出
*/
// 旧模块(待重构)
export * from './article'
// 新模块
export { AuthService } from './auth'
export { RoleService } from './role'
export { PermissionService } from './permission'
export { AccountService } from './account'
export { PlatformAccountService } from './platformAccount'
export { ShopAccountService } from './shopAccount'
export { ShopService } from './shop'
export { CardService } from './card'
// TODO: 按需添加其他业务模块
// export { PackageService } from './package'
// export { DeviceService } from './device'
// export { CommissionService } from './commission'
// export { SettingService } from './setting'

View File

@@ -0,0 +1,76 @@
/**
* 权限相关 API - 匹配后端实际接口
*/
import { BaseService } from '../BaseService'
import type {
Permission,
PermissionTreeNode,
PermissionQueryParams,
CreatePermissionParams,
UpdatePermissionParams,
BaseResponse,
PaginationResponse
} from '@/types/api'
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
* 用于角色分配权限时的树形选择
*/
static getPermissionTree(): Promise<BaseResponse<PermissionTreeNode[]>> {
return this.get<BaseResponse<PermissionTreeNode[]>>('/api/admin/permissions/tree')
}
/**
* 获取权限详情
* GET /api/admin/permissions/{id}
* @param id 权限ID
*/
static getPermission(id: number): Promise<BaseResponse<Permission>> {
return this.getOne<Permission>(`/api/admin/permissions/${id}`)
}
/**
* 创建权限
* POST /api/admin/permissions
* @param data 权限数据
*/
static createPermission(data: CreatePermissionParams): Promise<BaseResponse> {
return this.create('/api/admin/permissions', data)
}
/**
* 更新权限
* PUT /api/admin/permissions/{id}
* @param id 权限ID
* @param data 权限数据
*/
static updatePermission(
id: number,
data: UpdatePermissionParams
): Promise<BaseResponse> {
return this.update(`/api/admin/permissions/${id}`, data)
}
/**
* 删除权限
* DELETE /api/admin/permissions/{id}
* @param id 权限ID
*/
static deletePermission(id: number): Promise<BaseResponse> {
return this.remove(`/api/admin/permissions/${id}`)
}
}

View File

@@ -0,0 +1,147 @@
/**
* 平台账号相关 API - 匹配后端实际接口
*/
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)
}
}

104
src/api/modules/role.ts Normal file
View File

@@ -0,0 +1,104 @@
/**
* 角色相关 API - 匹配后端实际接口
*/
import { BaseService } from '../BaseService'
import type {
PlatformRole,
RoleQueryParams,
PlatformRoleFormData,
PermissionTreeNode,
BaseResponse,
PaginationResponse
} from '@/types/api'
export class RoleService extends BaseService {
/**
* 获取角色分页列表
* GET /api/admin/roles
* @param params 查询参数
*/
static getRoles(params?: RoleQueryParams): Promise<PaginationResponse<PlatformRole>> {
return this.getPage<PlatformRole>('/api/admin/roles', params)
}
/**
* 获取角色详情
* GET /api/admin/roles/{id}
* @param id 角色ID
*/
static getRole(id: number): Promise<BaseResponse<PlatformRole>> {
return this.getOne<PlatformRole>(`/api/admin/roles/${id}`)
}
/**
* 创建角色
* POST /api/admin/roles
* @param data 角色数据
*/
static createRole(data: PlatformRoleFormData): Promise<BaseResponse> {
return this.create('/api/admin/roles', data)
}
/**
* 更新角色
* PUT /api/admin/roles/{id}
* @param id 角色ID
* @param data 角色数据
*/
static updateRole(id: number, data: PlatformRoleFormData): Promise<BaseResponse> {
return this.update(`/api/admin/roles/${id}`, data)
}
/**
* 删除角色
* DELETE /api/admin/roles/{id}
* @param id 角色ID
*/
static deleteRole(id: number): Promise<BaseResponse> {
return this.remove(`/api/admin/roles/${id}`)
}
/**
* 更新角色状态
* PUT /api/admin/roles/{id}/status
* @param roleId 角色ID
* @param status 状态 (0-禁用, 1-启用)
*/
static updateRoleStatus(roleId: number, status: 0 | 1): Promise<BaseResponse> {
return this.put<BaseResponse>(`/api/admin/roles/${roleId}/status`, { status })
}
// ========== 权限相关 ==========
/**
* 获取角色权限
* GET /api/admin/roles/{id}/permissions
* @param roleId 角色ID
*/
static getRolePermissions(roleId: number): Promise<any> {
return this.get<any>(`/api/admin/roles/${roleId}/permissions`)
}
/**
* 分配权限给角色
* POST /api/admin/roles/{id}/permissions
* @param roleId 角色ID
* @param permissionIds 权限ID列表
*/
static assignPermissions(roleId: number, permissionIds: number[]): Promise<BaseResponse> {
return this.post<BaseResponse>(`/api/admin/roles/${roleId}/permissions`, {
perm_ids: permissionIds
})
}
/**
* 移除角色的单个权限
* DELETE /api/admin/roles/{role_id}/permissions/{perm_id}
* @param roleId 角色ID
* @param permId 权限ID
*/
static removePermission(roleId: number, permId: number): Promise<BaseResponse> {
return this.delete<BaseResponse>(`/api/admin/roles/${roleId}/permissions/${permId}`)
}
}

52
src/api/modules/shop.ts Normal file
View File

@@ -0,0 +1,52 @@
/**
* 店铺相关 API - 匹配后端实际接口
*/
import { BaseService } from '../BaseService'
import type {
ShopResponse,
ShopQueryParams,
CreateShopParams,
UpdateShopParams,
BaseResponse,
PaginationResponse
} from '@/types/api'
export class ShopService extends BaseService {
/**
* 获取店铺列表
* GET /api/admin/shops
* @param params 查询参数
*/
static getShops(params?: ShopQueryParams): Promise<PaginationResponse<ShopResponse>> {
return this.getPage<ShopResponse>('/api/admin/shops', params)
}
/**
* 创建店铺
* POST /api/admin/shops
* @param data 店铺数据
*/
static createShop(data: CreateShopParams): Promise<BaseResponse<ShopResponse>> {
return this.post<BaseResponse<ShopResponse>>('/api/admin/shops', data)
}
/**
* 更新店铺
* PUT /api/admin/shops/{id}
* @param id 店铺ID
* @param data 更新数据
*/
static updateShop(id: number, data: UpdateShopParams): Promise<BaseResponse<ShopResponse>> {
return this.put<BaseResponse<ShopResponse>>(`/api/admin/shops/${id}`, data)
}
/**
* 删除店铺
* DELETE /api/admin/shops/{id}
* @param id 店铺ID
*/
static deleteShop(id: number): Promise<BaseResponse> {
return this.delete<BaseResponse>(`/api/admin/shops/${id}`)
}
}

View File

@@ -0,0 +1,76 @@
/**
* 代理账号相关 API - 匹配后端实际接口
*/
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)
}
}

39
src/api/usersApi.ts Normal file
View File

@@ -0,0 +1,39 @@
import request from '@/utils/http'
import { BaseResponse, UserInfoResponse } from '@/types/api'
interface LoginParams {
username: string
password: string
device?: string
}
interface UserListParams {
current?: number
size?: number
}
export class UserService {
// 登录
static login(params: LoginParams) {
return request.post<BaseResponse>({
url: '/api/admin/login',
params
})
}
// 获取用户信息
// GET /api/admin/me
static getUserInfo() {
return request.get<BaseResponse<UserInfoResponse>>({
url: '/api/admin/me'
})
}
// 获取用户列表
static getUserList(params?: UserListParams) {
return request.get<BaseResponse>({
url: '/api/user/list',
params
})
}
}

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 954 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
src/assets/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
src/assets/img/user/bg.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

196
src/assets/styles/app.scss Normal file
View File

@@ -0,0 +1,196 @@
// 全局样式
@font-face {
font-family: 'DMSans';
font-style: normal;
font-weight: 400;
src: url(../fonts/DMSans.woff2) format('woff2');
}
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 400;
src: url(../fonts/Montserrat.woff2) format('woff2');
}
.btn-icon {
font-size: 10px;
}
.el-btn-red {
color: #fa6962 !important;
&:hover {
opacity: 0.9;
}
&:active {
opacity: 0.7;
}
}
// 顶部进度条颜色
#nprogress .bar {
background-color: color-mix(in srgb, var(--main-color) 65%, white);
}
// 处理移动端组件兼容性
@media screen and (max-width: $device-phone) {
* {
cursor: default !important;
}
.el-col2 {
margin-top: 15px;
}
}
// 背景滤镜
*,
::before,
::after {
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
// 色弱模式
.color-weak {
filter: invert(80%);
-webkit-filter: invert(80%);
}
#noop {
display: none;
}
// 语言切换选中样式
.langDropDownStyle {
// 选中项背景颜色
.is-selected {
background-color: rgba(var(--art-gray-200-rgb), 0.8) !important;
}
// 语言切换按钮菜单样式优化
.lang-btn-item {
.el-dropdown-menu__item {
padding-left: 13px !important;
padding-right: 6px !important;
margin-bottom: 3px !important;
}
&:last-child {
.el-dropdown-menu__item {
margin-bottom: 0 !important;
}
}
.menu-txt {
min-width: 60px;
display: block;
}
i {
font-size: 10px;
margin-left: 10px;
}
}
}
// 盒子默认边框
.page-content,
.art-custom-card {
border: 1px solid var(--art-card-border) !important;
}
// 盒子边框
[data-box-mode='border-mode'] {
.page-content,
.art-custom-card,
.art-table-card {
border: 1px solid var(--art-card-border) !important;
}
.layout-sidebar {
border-right: 1px solid var(--art-card-border) !important;
}
}
// 盒子阴影
[data-box-mode='shadow-mode'] {
.page-content,
.art-custom-card,
.art-table-card {
box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.04) !important;
border: 1px solid rgba(var(--art-gray-300-rgb), 0.3) !important;
}
.layout-sidebar {
border-right: 1px solid rgba(var(--art-gray-300-rgb), 0.4) !important;
}
}
html,
body {
touch-action: none; /* 禁用触摸事件 */
overflow-x: hidden; /* 禁用横向滚动 */
}
// 元素全屏
.el-full-screen {
position: fixed;
top: 0;
left: 0;
right: 0;
width: 100vw !important;
height: 100vh !important;
z-index: 500;
margin-top: 0;
padding: 15px;
box-sizing: border-box;
background-color: var(--art-main-bg-color);
.art-table-full-screen {
height: 100% !important;
}
}
// 表格卡片
.art-table-card {
flex: 1;
display: flex;
flex-direction: column;
margin-top: 15px;
border-radius: calc(var(--custom-radius) / 2 + 2px) !important;
.el-card__body {
height: 100%;
overflow: auto;
}
}
.flex-row-sb {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.mt-20 {
margin-top: 20px;
}
.flex-row-g20 {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: 20px;
}

View File

@@ -0,0 +1,11 @@
// 主题切换过渡优化,去除不适感
.theme-change {
* {
transition: 0s !important;
}
.el-switch__core,
.el-switch__action {
transition: all 0.3s !important;
}
}

215
src/assets/styles/dark.scss Normal file
View File

@@ -0,0 +1,215 @@
/*
* 深色主题
* 单页面移除深色主题 document.getElementsByTagName("html")[0].removeAttribute('class')
*/
$font-color: rgba(#ffffff, 0.7);
$background-color: #070707;
/* 覆盖element-plus默认深色背景色 */
html.dark {
// ✅ element-plus
// --el-bg-color: $background-color;
--el-text-color-regular: $font-color;
// ✅ 富文本编辑器
// 工具栏背景颜色
--w-e-toolbar-bg-color: var(--art-main-bg-color);
// 输入区域背景颜色
--w-e-textarea-bg-color: var(--art-main-bg-color);
// 工具栏文字颜色
--w-e-toolbar-color: var(--art-text-gray-600);
// 选中菜单颜色
--w-e-toolbar-active-bg-color: rgba(var(--art-gray-100-rgb), 0.8);
// 弹窗边框颜色
--w-e-toolbar-border-color: var(--art-border-dashed-color);
// 分割线颜色
--w-e-textarea-border-color: var(--art-border-dashed-color);
// 链接输入框边框颜色
--w-e-modal-button-border-color: var(--art-border-dashed-color);
// 表格头颜色
--w-e-textarea-slight-bg-color: var(--art-color);
// 按钮背景颜色
--w-e-modal-button-bg-color: var(--art-color);
}
.dark {
color: $font-color !important;
background: $background-color !important;
/* 全局文字颜色 */
body {
color: $font-color;
h1,
h2,
h3,
h4,
h5,
h6,
.lang .btn,
.layout-top-bar .user .name,
.dark-text {
color: $font-color !important;
}
}
// 图片降低亮度
img {
filter: brightness(0.92) saturate(1.25);
}
.editor-wrapper {
*:not(pre code *) {
color: inherit !important;
}
}
.img-cutter {
*:not([class^='el-']) {
color: inherit !important;
}
}
// ✅ 左侧菜单样式
.layout-sidebar,
.dual-menu {
.el-menu-dark {
// 选中颜色
.el-menu-item.is-active {
background: transparent;
}
.el-sub-menu__title {
.el-icon {
color: var(--art-gray-800) !important;
}
}
// 鼠标移入背景色
.el-sub-menu__title:hover,
.el-menu-item:not(.is-active):hover {
background: rgba(var(--art-gray-200-rgb), 0.6) !important;
}
[level-item='2'].is-active:not(.el-menu--collapse) {
&.is-active {
&:before {
margin-left: -10px !important;
}
}
}
.el-menu:not(.el-menu--collapse) {
// 选中颜色
.el-menu-item.is-active {
&:before {
content: '';
width: 5px;
height: 5px;
border-radius: 50%;
position: absolute;
top: 0;
bottom: 0;
margin: auto;
background: var(--main-color) !important;
transition: all 0.2s;
margin-left: -18px;
}
}
}
}
}
.page-content .article-list .item .left .outer > div {
border-right-color: var(--dark-border-color) !important;
}
// ✅ 富文本编辑器
// 分隔线
.w-e-bar-divider {
background-color: var(--art-gray-300) !important;
}
// 下拉选择框
.w-e-select-list {
background-color: var(--art-main-bg-color) !important;
border: 1px solid var(--art-border-dashed-color) !important;
}
/* 弹出框 */
.w-e-drop-panel {
border: 1px solid var(--art-border-dashed-color) !important;
}
/* 工具栏菜单 */
.w-e-bar-item-group .w-e-bar-item-menus-container {
background-color: var(--art-main-bg-color) !important;
border: 1px solid var(--art-border-dashed-color) !important;
}
/* 下拉选择框 hover 样式调整 */
.w-e-select-list ul li:hover,
/* 工具栏 hover 按钮背景颜色 */
.w-e-bar-item button:hover {
background-color: var(--art-color) !important;
}
/* 代码块 */
.w-e-text-container [data-slate-editor] pre > code {
background-color: var(--art-gray-100) !important;
border: 1px solid var(--art-border-dashed-color) !important;
text-shadow: none !important;
}
/* 引用 */
.w-e-text-container [data-slate-editor] blockquote {
border-left: 4px solid var(--art-gray-200) !important;
background-color: var(--art-color);
}
.editor-wrapper {
.w-e-text-container [data-slate-editor] .table-container th:last-of-type {
border-right: 1px solid var(--art-gray-200) !important;
}
.w-e-modal {
background-color: var(--art-color);
}
}
// 工作台标签文字颜色
.worktab .scroll-view .tabs li {
color: var(--art-text-gray-800) !important;
}
// 顶部按钮文字颜色
.layout-top-bar .btn-box .btn i,
.fast-enter-trigger .btn i {
color: var(--art-text-gray-700) !important;
}
}
// 移动端文字颜色
@media screen and (max-width: $device-phone) {
.dark {
$font-color: rgba(#ffffff, 0.8);
--el-text-color-regular: $font-color !important;
color: $font-color !important;
body {
color: $font-color !important;
h1,
h2,
h3,
h4,
h5,
h6,
.lang .btn,
.layout-top-bar .user .name {
color: $font-color !important;
}
}
}
}

View File

@@ -0,0 +1,16 @@
// 自定义Element 暗黑主题
@forward 'element-plus/theme-chalk/src/dark/var.scss' //
with (
$colors: (
//
'white': #ffffff,
'black': #000000,
'success': ('base': #13deb9),
'warning': ('base': #ffae1f),
'danger': ('base': #ff4d4f),
'error': ('base': #fa896b)
)
);
@use 'element-plus/theme-chalk/src/dark/css-vars.scss' as *;

View File

@@ -0,0 +1,34 @@
// https://github.com/element-plus/element-plus/blob/dev/packages/theme-chalk/src/common/var.scss
// 自定义Element 亮色主题
@forward 'element-plus/theme-chalk/src/common/var.scss' //
with (
//
$colors: (
//
'white': #ffffff,
'black': #000000,
'success': ('base': #13deb9),
'warning': ('base': #ffae1f),
'danger': ('base': #ff4d4f),
'error': ('base': #fa896b)
),
$button: (
//
'hover-bg-color': var(--el-color-primary-light-9),
'hover-border-color': var(--el-color-primary),
'border-color': var(--el-color-primary),
'text-color': var(--el-color-primary)
),
$messagebox: (
//
'border-radius': '12px'
),
$popover: (
//
'padding': '14px',
'border-radius': '10px'
)
);
@use 'element-plus/theme-chalk/src/index.scss' as *;

View File

@@ -0,0 +1,402 @@
// 优化 Element Plus 组件库默认样式
:root {
// 系统主色
--main-color: var(--el-color-primary);
--el-color-white: white !important;
--el-color-black: white !important;
// 输入框边框颜色
// --el-border-color: #E4E4E7 !important; // DCDFE6
// 按钮粗度
--el-font-weight-primary: 400 !important;
--el-component-custom-height: 36px !important;
--el-component-size: var(--el-component-custom-height) !important;
// 边框、按钮圆角...
--el-border-radius-base: calc(var(--custom-radius) / 3 + 2px) !important;
--el-border-radius-small: 10px !important;
--el-messagebox-border-radius: 10px !important;
.region .el-radio-button__original-radio:checked + .el-radio-button__inner {
color: var(--main-color);
}
}
// 优化菜单折叠展开动画(提升动画流畅度)
.el-menu.el-menu--inline {
transition: max-height 0.26s cubic-bezier(0.4, 0, 0.2, 1) !important;
}
// 优化菜单 item hover 动画(提升鼠标跟手感)
.el-sub-menu__title,
.el-menu-item {
transition: background-color 0s !important;
}
// -------------------------------- 修改 el-size=default 组件默认高度 start --------------------------------
// 修改 el-button 高度
.el-button--default {
height: var(--el-component-custom-height) !important;
}
// 修改 el-select 高度
.el-select--default {
.el-select__wrapper {
min-height: var(--el-component-custom-height) !important;
}
}
// 修改 el-checkbox-button 高度
.el-checkbox-button--default .el-checkbox-button__inner,
// 修改 el-radio-button 高度
.el-radio-button--default .el-radio-button__inner {
padding: 10px 15px !important;
}
// -------------------------------- 修改 el-size=default 组件默认高度 end --------------------------------
.el-pagination.is-background .btn-next,
.el-pagination.is-background .btn-prev,
.el-pagination.is-background .el-pager li {
border-radius: 6px;
}
.el-popover {
min-width: 80px;
}
.el-dialog {
border-radius: 100px !important;
border-radius: calc(var(--custom-radius) / 1.2 + 2px) !important;
overflow: hidden;
}
.el-dialog__header {
.el-dialog__title {
font-size: 16px;
}
}
.el-dialog__body {
padding: 25px 0 !important;
position: relative; // 为了兼容 el-pagination 样式,需要设置 relative不然会影响 el-pagination 的样式,比如 el-pagination__jump--small 会被影响,导致 el-pagination__jump--small 按钮无法点击,详见 URL_ADDRESS.com/element-plus/element-plus/issues/5684#issuecomment-1176299275;
}
.el-dialog.el-dialog-border {
.el-dialog__body {
// 上边框
&::before,
// 下边框
&::after {
content: '';
position: absolute;
left: -16px;
width: calc(100% + 32px);
height: 1px;
background-color: rgba(var(--art-gray-300-rgb), 0.56);
}
&::before {
top: 0;
}
&::after {
bottom: 0;
}
}
}
// ✅ el-message 样式优化
.el-message {
background-color: var(--art-main-bg-color) !important;
border: 0 !important;
box-shadow:
0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 9px 28px 8px rgba(0, 0, 0, 0.05) !important;
p {
color: #515a6e !important;
font-size: 13px;
}
}
// 修改 el-dropdown 样式
.el-dropdown-menu {
padding: 6px !important;
border-radius: 10px !important;
border: none !important;
.el-dropdown-menu__item {
padding: 6px 16px !important;
border-radius: 6px !important;
&:hover:not(.is-disabled) {
color: var(--art-gray-900) !important;
background-color: var(--art-gray-200) !important;
}
}
}
// 隐藏 select、dropdown 的三角
.el-select__popper,
.el-dropdown__popper {
margin-top: -6px !important;
.el-popper__arrow {
display: none;
}
}
.el-dropdown-selfdefine:focus {
outline: none !important;
}
// 处理移动端组件兼容性
@media screen and (max-width: $device-phone) {
.el-message-box,
.el-message,
.el-dialog {
width: calc(100% - 24px) !important;
}
.el-date-picker.has-sidebar.has-time {
width: calc(100% - 24px);
left: 12px !important;
}
.el-picker-panel *[slot='sidebar'],
.el-picker-panel__sidebar {
display: none;
}
.el-picker-panel *[slot='sidebar'] + .el-picker-panel__body,
.el-picker-panel__sidebar + .el-picker-panel__body {
margin-left: 0;
}
}
// 修改el-button样式
.el-button {
&.el-button--text {
background-color: transparent !important;
padding: 0 !important;
span {
margin-left: 0 !important;
}
}
}
// 修改el-tag样式
.el-tag {
height: 26px !important;
line-height: 26px !important;
border: 0 !important;
border-radius: 6px !important;
font-weight: bold;
transition: all 0s !important;
}
.el-checkbox-group {
&.el-table-filter__checkbox-group label.el-checkbox {
height: 17px !important;
.el-checkbox__label {
font-weight: 400 !important;
}
}
}
.el-checkbox {
.el-checkbox__inner {
width: 18px !important;
height: 18px !important;
border-radius: 4px !important;
&::before {
content: '';
height: 3px !important;
top: 6px !important;
background-color: #fff !important;
transform: scale(0.6) !important;
}
&::after {
width: 4px;
height: 8px;
left: 0;
right: 0;
top: 0;
bottom: 4px;
margin: auto;
border: 2px solid var(--el-checkbox-checked-icon-color);
border-left: 0;
border-top: 0;
}
}
}
.el-notification .el-notification__icon {
font-size: 22px !important;
}
// 修改 el-message-box 样式
.el-message-box__headerbtn .el-message-box__close,
.el-dialog__headerbtn .el-dialog__close {
color: var(--art-gray-500) !important;
top: 7px !important;
right: 7px !important;
padding: 7px !important;
border-radius: 5px !important;
transition: all 0.3s !important;
&:hover {
background-color: var(--art-gray-200) !important;
color: var(--art-gray-800) !important;
}
}
.el-message-box {
padding: 25px 20px !important;
}
.el-message-box__title {
font-weight: 500 !important;
}
.el-table__column-filter-trigger i {
color: var(--main-color) !important;
margin: -3px 0 0 2px;
}
// 去除 el-dropdown 鼠标放上去出现的边框
.el-tooltip__trigger:focus-visible {
outline: unset;
}
// ipad 表单右侧按钮优化
@media screen and (max-width: $device-ipad-pro) {
.el-table-fixed-column--right {
padding-right: 0 !important;
.el-button {
margin: 5px 10px 5px 0 !important;
}
}
}
.login-out-dialog {
padding: 30px 20px !important;
border-radius: 10px !important;
}
// 修改 dialog 动画
.dialog-fade-enter-active {
.el-dialog {
animation: dialog-open 0.3s cubic-bezier(0.32, 0.14, 0.15, 0.86);
// 修复 el-dialog 动画后宽度不自适应问题
.el-select__selected-item {
display: inline-block;
}
}
}
.dialog-fade-leave-active {
animation: fade-out 0.2s linear;
.el-dialog {
animation: dialog-close 0.2s cubic-bezier(0.78, 0.14, 0.15, 0.86);
}
}
@keyframes dialog-open {
0% {
opacity: 0;
transform: scale(0.2);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes dialog-close {
0% {
opacity: 1;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(0.2);
}
}
// 遮罩层动画
@keyframes fade-out {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
// 修改 el-select 样式
.el-select__popper:not(.el-tree-select__popper) {
.el-select-dropdown__list {
padding: 5px !important;
.el-select-dropdown__item {
height: 34px !important;
line-height: 34px !important;
border-radius: 6px !important;
&.is-selected {
color: var(--art-gray-900) !important;
font-weight: 400 !important;
background-color: var(--art-gray-200) !important;
margin-bottom: 4px !important;
}
&:hover {
background-color: var(--art-gray-200) !important;
}
}
.el-select-dropdown__item:hover ~ .is-selected,
.el-select-dropdown__item.is-selected:has(~ .el-select-dropdown__item:hover) {
background-color: transparent !important;
}
}
}
// 修改 el-tree-select 样式
.el-tree-select__popper {
.el-select-dropdown__list {
padding: 5px !important;
.el-tree-node {
.el-tree-node__content {
height: 36px !important;
border-radius: 6px !important;
&:hover {
background-color: var(--art-gray-200) !important;
}
}
}
}
}
// 实现水波纹在文字下面效果
.el-button > span {
position: relative;
z-index: 10;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,157 @@
// sass 混合宏(函数)
/**
* 溢出省略号
* @param {Number} 行数
*/
@mixin ellipsis($rowCount: 1) {
@if $rowCount <=1 {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} @else {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: $rowCount;
-webkit-box-orient: vertical;
}
}
/**
* 控制用户能否选中文本
* @param {String} 类型
*/
@mixin userSelect($value: none) {
user-select: $value;
-moz-user-select: $value;
-ms-user-select: $value;
-webkit-user-select: $value;
}
// 绝对定位居中
@mixin absoluteCenter() {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
}
/**
* css3动画
*
*/
@mixin animation(
$from: (
width: 0px
),
$to: (
width: 100px
),
$name: mymove,
$animate: mymove 2s 1 linear infinite
) {
-webkit-animation: $animate;
-o-animation: $animate;
animation: $animate;
@keyframes #{$name} {
from {
@each $key, $value in $from {
#{$key}: #{$value};
}
}
to {
@each $key, $value in $to {
#{$key}: #{$value};
}
}
}
@-webkit-keyframes #{$name} {
from {
@each $key, $value in $from {
$key: $value;
}
}
to {
@each $key, $value in $to {
$key: $value;
}
}
}
}
// 圆形盒子
@mixin circle($size: 11px, $bg: #fff) {
border-radius: 50%;
width: $size;
height: $size;
line-height: $size;
text-align: center;
background: $bg;
}
// placeholder
@mixin placeholder($color: #bbb) {
// Firefox
&::-moz-placeholder {
color: $color;
opacity: 1;
}
// Internet Explorer 10+
&:-ms-input-placeholder {
color: $color;
}
// Safari and Chrome
&::-webkit-input-placeholder {
color: $color;
}
&:placeholder-shown {
text-overflow: ellipsis;
}
}
//背景透明文字不透明。兼容IE8
@mixin betterTransparentize($color, $alpha) {
$c: rgba($color, $alpha);
$ie_c: ie_hex_str($c);
background: rgba($color, 1);
background: $c;
background: transparent \9;
zoom: 1;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#{$ie_c}, endColorstr=#{$ie_c});
-ms-filter: 'progid:DXImageTransform.Microsoft.gradient(startColorstr=#{$ie_c}, endColorstr=#{$ie_c})';
}
//添加浏览器前缀
@mixin browserPrefix($propertyName, $value) {
@each $prefix in -webkit-, -moz-, -ms-, -o-, '' {
#{$prefix}#{$propertyName}: $value;
}
}
// 边框
@mixin border($color: red) {
border: 1px solid $color;
}
// 背景滤镜
@mixin backdropBlur() {
--tw-backdrop-blur: blur(30px);
-webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness)
var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate)
var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate)
var(--tw-backdrop-sepia);
backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast)
var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert)
var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
}

View File

@@ -0,0 +1,8 @@
// 移动端样式处理
// 去除移动端点击背景色
@media screen and (max-width: $device-ipad-pro) {
* {
-webkit-tap-highlight-color: transparent;
}
}

View File

@@ -0,0 +1,117 @@
/*
Atom One Dark by Daniel Gamage
Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax
base: #282c34
mono-1: #abb2bf
mono-2: #818896
mono-3: #5c6370
hue-1: #56b6c2
hue-2: #61aeee
hue-3: #c678dd
hue-4: #98c379
hue-5: #e06c75
hue-5-2: #be5046
hue-6: #d19a66
hue-6-2: #e6c07b
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
// color: #abb2bf;
// background: #282c34;
color: #a6accd;
}
.hljs-string,
.hljs-section,
.hljs-selector-class,
.hljs-template-variable,
.hljs-deletion {
color: #aed07e !important;
}
.hljs-comment,
.hljs-quote {
color: #6f747d;
}
.hljs-doctag,
.hljs-keyword,
.hljs-formula {
color: #c792ea;
}
.hljs-section,
.hljs-name,
.hljs-selector-tag,
.hljs-deletion,
.hljs-subst {
color: #c86068;
}
.hljs-literal {
color: #56b6c2;
}
.hljs-string,
.hljs-regexp,
.hljs-addition,
.hljs-attribute,
.hljs-meta-string {
color: #abb2bf;
}
.hljs-attribute {
color: #c792ea;
}
.hljs-function {
color: #c792ea;
}
.hljs-type {
color: #f07178;
}
.hljs-title {
color: #82aaff !important;
}
.hljs-built_in,
.hljs-class {
color: #82aaff;
}
// 括号
.hljs-params {
color: #a6accd;
}
.hljs-attr,
.hljs-variable,
.hljs-template-variable,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-number {
color: #de7e61;
}
.hljs-symbol,
.hljs-bullet,
.hljs-link,
.hljs-meta,
.hljs-selector-id {
color: #61aeee;
}
.hljs-strong {
font-weight: bold;
}
.hljs-link {
text-decoration: underline;
}

View File

@@ -0,0 +1,150 @@
@charset "UTF-8";
body,
dl,
dt,
dd,
ul,
ol,
li,
pre,
form,
fieldset,
input,
p,
blockquote,
th,
td {
font-weight: 400;
margin: 0;
padding: 0;
}
h1,
h2,
h3,
h4,
h4,
h5 {
margin: 0;
padding: 0;
color: var(--art-text-gray-800);
}
body {
color: var(--art-text-gray-700);
text-align: left;
font-family:
Inter, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei',
'微软雅黑', Arial, sans-serif;
}
select {
font-size: 12px;
}
table {
border-collapse: collapse;
}
fieldset,
img {
border: 0none;
}
fieldset {
margin: 0;
padding: 0;
}
fieldset p {
margin: 0;
padding: 0008px;
}
legend {
display: none;
}
address,
caption,
em,
strong,
th,
i {
font-style: normal;
font-weight: 400;
}
table caption {
margin-left: -1px;
}
hr {
border-bottom: 1pxsolid #ffffff;
border-top: 1pxsolid #e4e4e4;
border-width: 1px0;
clear: both;
height: 2px;
margin: 5px0;
overflow: hidden;
}
ol,
ul {
list-style-image: none;
list-style-position: outside;
list-style-type: none;
}
caption,
th {
text-align: left;
}
q:before,
q:after,
blockquote:before,
blockquote:after {
content: ;
}
/*滚动条*/
/*滚动条整体部分,必须要设置*/
::-webkit-scrollbar {
width: 8px !important;
height: 0 !important;
}
/*滚动条的轨道*/
::-webkit-scrollbar-track {
background-color: var(--art-text-gray-100);
}
/*滚动条的滑块按钮*/
::-webkit-scrollbar-thumb {
border-radius: 5px;
background-color: #cccccc !important;
transition: all 0.2s;
-webkit-transition: all 0.2s;
}
::-webkit-scrollbar-thumb:hover {
background-color: #b0abab !important;
}
/*滚动条的上下两端的按钮*/
::-webkit-scrollbar-button {
height: 0px;
width: 0;
}
.dark {
::-webkit-scrollbar-track {
background-color: var(--art-bg-color);
}
::-webkit-scrollbar-thumb {
background-color: rgba(var(--art-gray-300-rgb), 0.8) !important;
}
}

View File

@@ -0,0 +1,63 @@
// 定义基础变量
$bg-animation-color-light: #000;
$bg-animation-color-dark: #fff;
$bg-animation-duration: 0.5s;
html {
--bg-animation-color: $bg-animation-color-light;
&.dark {
--bg-animation-color: $bg-animation-color-dark;
}
// View transition styles
&::view-transition-old(*) {
animation: none;
}
&::view-transition-new(*) {
animation: clip $bg-animation-duration ease-in;
}
&::view-transition-old(root) {
z-index: 1;
}
&::view-transition-new(root) {
z-index: 9999;
}
&.dark {
&::view-transition-old(*) {
animation: clip $bg-animation-duration ease-in reverse;
}
&::view-transition-new(*) {
animation: none;
}
&::view-transition-old(root) {
z-index: 9999;
}
&::view-transition-new(root) {
z-index: 1;
}
}
}
// 定义动画
@keyframes clip {
from {
clip-path: circle(0% at var(--x) var(--y));
}
to {
clip-path: circle(var(--r) at var(--x) var(--y));
}
}
// body 相关样式
body {
background-color: var(--bg-animation-color);
}

View File

@@ -0,0 +1,97 @@
@use 'sass:map';
// === 变量区域 ===
$transition: (
duration: 0.26s,
// 动画持续时间
distance: 20px,
// 滑动动画的移动距离
easing: cubic-bezier(0.4, 0, 0.2, 1),
// 默认缓动函数
fade-easing: ease // 淡入淡出专用的缓动函数
);
// 抽取配置值函数,提高可复用性
@function transition-config($key) {
@return map.get($transition, $key);
}
// 变量简写
$duration: transition-config('duration');
$distance: transition-config('distance');
$easing: transition-config('easing');
$fade-easing: transition-config('fade-easing');
// === 动画类 ===
// 淡入淡出动画
.fade {
&-enter-active,
&-leave-active {
transition: opacity $duration $fade-easing;
will-change: opacity;
}
&-enter-from,
&-leave-to {
opacity: 0;
}
&-enter-to,
&-leave-from {
opacity: 1;
}
}
// 滑动动画通用样式
@mixin slide-transition($direction) {
$distance-x: 0;
$distance-y: 0;
@if $direction == 'left' {
$distance-x: -$distance;
} @else if $direction == 'right' {
$distance-x: $distance;
} @else if $direction == 'top' {
$distance-y: -$distance;
} @else if $direction == 'bottom' {
$distance-y: $distance;
}
&-enter-active,
&-leave-active {
transition:
opacity $duration $easing,
transform $duration $easing;
will-change: opacity, transform;
}
&-enter-from {
opacity: 0;
transform: translate3d($distance-x, $distance-y, 0);
}
&-enter-to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
&-leave-to {
opacity: 0;
transform: translate3d(-$distance-x, -$distance-y, 0);
}
}
// 滑动动画方向类
.slide-left {
@include slide-transition('left');
}
.slide-right {
@include slide-transition('right');
}
.slide-top {
@include slide-transition('top');
}
.slide-bottom {
@include slide-transition('bottom');
}

150
src/assets/styles/tree.scss Normal file
View File

@@ -0,0 +1,150 @@
// 自定义Element树形结构组件样式
.tree .custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
.tree .tree .el-tree-node__content {
height: 38px;
line-height: 38px;
}
.el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content {
background-color: #409eff;
color: #fff;
}
.el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content i {
color: #fff;
}
.tree .custom-tree-node .icon {
font-size: 13px;
color: #409eff;
}
.tree .custom-tree-node .btn {
font-size: 13px;
display: none;
padding: 6px;
position: relative;
}
.tree .custom-tree-node:hover .icon {
color: #409eff;
}
.tree .el-tree-node__content:hover {
color: #606060;
// background: #409EFF;
background: #f0f7ff;
}
.tree .custom-tree-node:hover .btn {
display: inline;
}
.tree .custom-tree-node .btn:hover ul {
display: inline;
}
.tree .custom-tree-node .btn ul {
width: 120px;
background: #fff;
position: absolute;
top: 26px;
right: 0;
display: none;
z-index: 999;
border: 1px solid #f0f0f0;
// box-shadow: 0 4px 4px 2px #f2f2f2;
}
.tree .custom-tree-node .btn ul li {
padding: 10px 15px;
color: #666666;
box-sizing: border-box;
}
.tree .custom-tree-node .btn ul li:hover {
color: #333;
background: #f5f5f5;
}
.tree .el-tree-node.is-expanded > .el-tree-node__children {
overflow: inherit;
}
.tree .el-tree > .el-tree-node:after {
border-top: none;
}
.tree .el-tree-node {
position: relative;
}
.tree .el-tree-node__expand-icon.is-leaf {
display: none;
}
.tree .el-tree-node__children {
padding-left: 16px;
}
.tree .el-tree-node :last-child:before {
height: 38px;
}
.tree .el-tree-node :last-child:before {
height: 17px;
}
.tree .el-tree > .el-tree-node:before {
border-left: none;
}
.tree .el-tree > .el-tree-node:after {
border-top: none;
}
.tree .el-tree-node:before {
content: '';
position: absolute;
left: -4px;
right: auto;
border-width: 1px;
}
.tree .el-tree-node:after {
content: '';
left: -4px;
position: absolute;
right: auto;
border-width: 1px;
}
.tree .el-tree-node:before {
border-left: 1px dashed #dcdfe6;
bottom: 0px;
height: 100%;
top: -3px;
width: 1px;
left: 14px;
}
.tree .el-tree-node:after {
border-top: 1px dashed #dcdfe6;
height: 20px;
top: 13px;
left: 15px;
width: 12px;
}
.tree-color {
background: #f7fafe;
}

View File

@@ -0,0 +1,251 @@
// Light 主题变量 Dark 主题变量
:root {
// Theme color
--art-primary: 93, 135, 255;
--art-secondary: 73, 190, 255;
--art-error: 250, 137, 107;
--art-info: 83, 155, 255;
--art-success: 19, 222, 185;
--art-warning: 255, 174, 31;
--art-danger: 255, 77, 79;
// Theme background color
--art-bg-primary: 236, 242, 255;
--art-bg-secondary: 232, 247, 255;
--art-bg-success: 230, 255, 250;
--art-bg-error: 253, 237, 232;
--art-bg-info: 235, 243, 254;
--art-bg-warning: 254, 245, 229;
--art-bg-danger: 253, 237, 232;
--art-hoverColor: 246, 249, 252;
--art-grey100: 242, 246, 250;
--art-grey200: 234, 239, 244;
--art-color: #ffffff;
--art-light: #f9f9f9;
--art-dark: #1e2129;
// Background color | Hover color
--art-text-muted: #99a1b7;
--art-gray-100: #f9f9f9;
--art-gray-100-rgb: 249, 249, 249;
--art-gray-200: #f1f1f4;
--art-gray-200-rgb: 241, 241, 244;
--art-gray-300: #dbdfe9;
--art-gray-300-rgb: 219, 223, 233;
--art-gray-400: #c4cada;
--art-gray-400-rgb: 196, 202, 218;
--art-gray-500: #99a1b7;
--art-gray-500-rgb: 153, 161, 183;
--art-gray-600: #78829d;
--art-gray-600-rgb: 120, 130, 157;
--art-gray-700: #4b5675;
--art-gray-700-rgb: 75, 86, 117;
--art-gray-800: #252f4a;
--art-gray-800-rgb: 37, 47, 74;
--art-gray-900: #071437;
--art-gray-900-rgb: 7, 20, 55;
// Text color
--art-text-muted: #99a1b7;
--art-text-gray-100: #f9f9f9;
--art-text-gray-200: #f1f1f4;
--art-text-gray-300: #dbdfe9;
--art-text-gray-400: #c4cada;
--art-text-gray-500: #99a1b7;
--art-text-gray-600: #78829d;
--art-text-gray-700: #4b5675;
--art-text-gray-800: #252f4a;
--art-text-gray-900: #071437;
// Border
--art-border-color: #eaebf1;
--art-border-dashed-color: #dbdfe9;
--art-root-card-border-color: #f1f1f4;
// Shadow
--art-box-shadow-xs: 0 0.1rem 0.75rem 0.25rem rgba(0, 0, 0, 0.05);
--art-box-shadow-sm: 0 0.1rem 1rem 0.25rem rgba(0, 0, 0, 0.05);
--art-box-shadow: 0 0.5rem 1.5rem 0.5rem rgba(0, 0, 0, 0.075);
--art-box-shadow-lg: 0 1rem 2rem 1rem rgba(0, 0, 0, 0.1);
// Root card box、shadow
--art-root-card-box-shadow: 0px 3px 4px 0px rgba(0, 0, 0, 0.03);
--art-root-card-border-color: #f1f1f4;
// Theme background color
--art-bg-color: #fafbfc; // 最底部背景颜色
--art-main-bg-color: #ffffff;
}
// Dark 主题变量
html.dark {
// Theme color
--art-primary: 93, 135, 255;
--art-secondary: 73, 190, 255;
--art-error: 250, 137, 107;
--art-info: 83, 155, 255;
--art-success: 19, 222, 185;
--art-warning: 255, 174, 31;
--art-danger: 255, 77, 79;
// Theme background color
--art-bg-primary: 37, 54, 98;
--art-bg-secondary: 28, 69, 93;
--art-bg-success: 27, 60, 72;
--art-bg-error: 75, 49, 61;
--art-bg-info: 34, 54, 98;
--art-bg-warning: 77, 58, 42;
--art-bg-danger: 100, 49, 61;
--art-hoverColor: 51, 63, 85;
--art-grey100: 51, 63, 85;
--art-grey200: 70, 86, 112;
--art-color: #000000;
--art-light: #1b1c22;
--art-dark: #272a34;
// Background color | Hover color
--art-text-muted: #636674;
--art-gray-100: #1b1c22;
--art-gray-100-rgb: 27, 28, 34;
--art-gray-200: #26272f;
--art-gray-200-rgb: 38, 39, 47;
--art-gray-300: #363843;
--art-gray-300-rgb: 54, 56, 67;
--art-gray-400: #464852;
--art-gray-400-rgb: 70, 72, 82;
--art-gray-500: #636674;
--art-gray-500-rgb: 99, 102, 116;
--art-gray-600: #808290;
--art-gray-600-rgb: 128, 130, 144;
--art-gray-700: #9a9cae;
--art-gray-700-rgb: 154, 156, 174;
--art-gray-800: #b5b7c8;
--art-gray-800-rgb: 181, 183, 200;
--art-gray-900: #f5f5f5;
--art-gray-900-rgb: 245, 245, 245;
// Text color
--art-text-muted: #636674;
--art-text-gray-100: #1b1c22;
--art-text-gray-200: #26272f;
--art-text-gray-300: #363843;
--art-text-gray-400: #464852;
--art-text-gray-500: #636674;
--art-text-gray-600: #808290;
--art-text-gray-700: #9a9cae;
--art-text-gray-800: #b5b7c8;
--art-text-gray-900: #f5f5f5;
// Border
--art-border-color: #26272f;
--art-border-dashed-color: #363843;
--art-root-card-border-color: #1e2027;
// Shadow
--art-box-shadow-xs: 0 0.1rem 0.75rem 0.25rem rgba(0, 0, 0, 0.05);
--art-box-shadow-sm: 0 0.1rem 1rem 0.25rem rgba(0, 0, 0, 0.05);
--art-box-shadow: 0 0.5rem 1.5rem 0.5rem rgba(0, 0, 0, 0.075);
--art-box-shadow-lg: 0 1rem 2rem 1rem rgba(0, 0, 0, 0.1);
// Root card box、shadow
--art-root-card-box-shadow: none;
--art-root-card-border-color: #1e2027;
// Theme background color
--art-bg-color: #070707;
--art-main-bg-color: #161618;
}
// CSS 全局变量
:root {
--art-card-border: rgba(var(--art-gray-300-rgb), 0.6); // 卡片边框颜色
--art-card-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.04); // 卡片阴影
}
// 媒体查询-设备尺寸
// notebook
$device-notebook: 1600px;
// ipad pro
$device-ipad-pro: 1180px;
// ipad
$device-ipad: 800px;
// ipad-竖屏
$device-ipad-vertical: 900px;
// mobile
$device-phone: 500px;
.bg-primary {
background-color: rgb(var(--art-bg-primary)) !important;
color: rgb(var(--art-primary)) !important;
}
.bg-secondary {
background-color: rgb(var(--art-bg-secondary)) !important;
color: rgb(var(--art-secondary)) !important;
border: 1px solid var(--art-secondary);
}
.bg-warning {
background-color: rgb(var(--art-bg-warning)) !important;
color: rgb(var(--art-warning)) !important;
}
.bg-error {
background-color: rgb(var(--art-bg-error)) !important;
color: rgb(var(--art-error)) !important;
}
.bg-success {
background-color: rgb(var(--art-bg-success)) !important;
color: rgb(var(--art-success)) !important;
}
.bg-danger {
background-color: rgb(var(--art-bg-danger)) !important;
color: rgb(var(--art-danger)) !important;
}
.bg-grey100 {
background-color: rgb(var(--art-grey100)) !important;
}
.bg-grey200 {
background-color: rgb(var(--art-grey200)) !important;
}
.bg-hoverColor {
background-color: rgb(var(--art-hoverColor)) !important;
}
.text-primary {
color: rgb(var(--art-primary)) !important;
}
.text-secondary {
color: rgb(var(--art-secondary)) !important;
}
.text-error {
color: rgb(var(--art-error)) !important;
}
.text-danger {
color: rgb(var(--art-danger)) !important;
}
.text-info {
color: rgb(var(--art-info)) !important;
}
.text-success {
color: rgb(var(--art-success)) !important;
}
.text-warning {
color: rgb(var(--art-warning)) !important;
}

32
src/assets/svg/loading.ts Normal file
View File

@@ -0,0 +1,32 @@
// 自定义四点旋转SVG
export const fourDotsSpinnerSvg = `
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40">
<style>
.spinner {
transform-origin: center;
animation: rotate 1.6s linear infinite;
}
.dot {
fill: var(--main-color);
animation: fade 1.6s infinite;
}
.dot:nth-child(1) { animation-delay: 0s; }
.dot:nth-child(2) { animation-delay: 0.5s; }
.dot:nth-child(3) { animation-delay: 1s; }
.dot:nth-child(4) { animation-delay: 1.5s; }
@keyframes rotate {
100% { transform: rotate(360deg); }
}
@keyframes fade {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
</style>
<g class="spinner">
<circle class="dot" cx="20" cy="8" r="4"/>
<circle class="dot" cx="32" cy="20" r="4"/>
<circle class="dot" cx="20" cy="32" r="4"/>
<circle class="dot" cx="8" cy="20" r="4"/>
</g>
</svg>
`

View File

@@ -0,0 +1,127 @@
<template>
<el-tree-select
v-model="selectedValue"
:data="agentTreeData"
:props="treeProps"
:placeholder="placeholder"
:clearable="clearable"
:disabled="disabled"
:filterable="filterable"
:check-strictly="checkStrictly"
:multiple="multiple"
:size="size"
:render-after-expand="false"
@change="handleChange"
@visible-change="handleVisibleChange"
>
<template #default="{ data }">
<div class="agent-node">
<span class="agent-name">{{ data.name }}</span>
<span v-if="data.level" class="agent-level">{{ data.level }}</span>
</div>
</template>
</el-tree-select>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import type { Agent } from '@/types/api'
interface Props {
modelValue?: string | number | (string | number)[]
placeholder?: string
clearable?: boolean
disabled?: boolean
filterable?: boolean
checkStrictly?: boolean
multiple?: boolean
size?: 'large' | 'default' | 'small'
// 预加载的代理商树数据
agents?: Agent[]
// 远程获取方法
fetchMethod?: () => Promise<Agent[]>
}
interface Emits {
(e: 'update:modelValue', value: string | number | (string | number)[] | undefined): void
(e: 'change', value: string | number | (string | number)[] | undefined): void
}
const props = withDefaults(defineProps<Props>(), {
placeholder: '请选择代理商',
clearable: true,
disabled: false,
filterable: true,
checkStrictly: false,
multiple: false,
size: 'default'
})
const emit = defineEmits<Emits>()
const loading = ref(false)
const agentTreeData = ref<Agent[]>([])
const treeProps = {
value: 'id',
label: 'name',
children: 'children'
}
const selectedValue = computed({
get: () => props.modelValue,
set: (val) => {
emit('update:modelValue', val)
}
})
const handleChange = (value: string | number | (string | number)[] | undefined) => {
emit('change', value)
}
const handleVisibleChange = (visible: boolean) => {
if (visible && agentTreeData.value.length === 0) {
loadAgents()
}
}
const loadAgents = async () => {
if (props.agents) {
agentTreeData.value = props.agents
} else if (props.fetchMethod) {
loading.value = true
try {
agentTreeData.value = await props.fetchMethod()
} finally {
loading.value = false
}
}
}
onMounted(() => {
if (props.agents) {
agentTreeData.value = props.agents
}
})
</script>
<style scoped lang="scss">
.agent-node {
display: flex;
align-items: center;
gap: 8px;
.agent-name {
font-weight: 500;
color: var(--el-text-color-primary);
}
.agent-level {
font-size: 12px;
padding: 2px 8px;
border-radius: 4px;
background-color: var(--el-color-primary-light-9);
color: var(--el-color-primary);
}
}
</style>

View File

@@ -0,0 +1,153 @@
<template>
<el-dialog
v-model="visible"
:title="title"
:width="width"
:close-on-click-modal="false"
:close-on-press-escape="false"
@closed="handleClosed"
>
<div class="batch-operation-content">
<!-- 选中项提示 -->
<el-alert
v-if="selectedCount > 0"
:title="`已选择 ${selectedCount} 项`"
type="info"
:closable="false"
show-icon
/>
<!-- 操作表单 -->
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
class="operation-form"
>
<slot name="form" :form-data="formData" />
</el-form>
<!-- 确认提示 -->
<el-alert
v-if="confirmMessage"
:title="confirmMessage"
type="warning"
:closable="false"
show-icon
class="confirm-alert"
/>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleCancel">取消</el-button>
<el-button
type="primary"
:loading="loading"
@click="handleConfirm"
>
确定
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage } from 'element-plus'
interface Props {
modelValue: boolean
title: string
width?: string | number
selectedCount?: number
confirmMessage?: string
formRules?: FormRules
}
interface Emits {
(e: 'update:modelValue', value: boolean): void
(e: 'confirm', formData: Record<string, any>): void | Promise<void>
(e: 'cancel'): void
}
const props = withDefaults(defineProps<Props>(), {
width: '600px',
selectedCount: 0
})
const emit = defineEmits<Emits>()
const formRef = ref<FormInstance>()
const formData = ref<Record<string, any>>({})
const loading = ref(false)
const visible = computed({
get: () => props.modelValue,
set: (val) => {
emit('update:modelValue', val)
}
})
const handleConfirm = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
loading.value = true
try {
await emit('confirm', formData.value)
visible.value = false
ElMessage.success('操作成功')
} catch (error) {
console.error('批量操作失败:', error)
ElMessage.error('操作失败')
} finally {
loading.value = false
}
} catch {
ElMessage.warning('请检查表单填写')
}
}
const handleCancel = () => {
visible.value = false
emit('cancel')
}
const handleClosed = () => {
formRef.value?.resetFields()
formData.value = {}
}
// 暴露方法供父组件调用
defineExpose({
formData
})
</script>
<style scoped lang="scss">
.batch-operation-content {
display: flex;
flex-direction: column;
gap: 16px;
.operation-form {
margin-top: 16px;
}
.confirm-alert {
margin-top: 8px;
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
</style>

View File

@@ -0,0 +1,293 @@
<template>
<ElDialog
v-model="visible"
:title="title"
:width="width"
align-center
:before-close="handleClose"
>
<ElForm
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
>
<!-- 选择下拉项表单项 -->
<ElFormItem
v-if="showSelect"
:label="selectLabel"
:prop="selectProp"
>
<ElSelect
v-model="formData[selectProp]"
:placeholder="selectPlaceholder"
filterable
remote
:remote-method="handleRemoteSearch"
:loading="selectLoading"
clearable
style="width: 100%"
>
<ElOption
v-for="item in selectOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</ElSelect>
</ElFormItem>
<!-- 充值金额输入框 -->
<ElFormItem
v-if="showAmount"
label="充值金额"
prop="amount"
>
<ElInput
v-model="formData.amount"
placeholder="请输入充值金额"
>
<template #append></template>
</ElInput>
</ElFormItem>
<!-- 备注信息 -->
<ElFormItem
v-if="showRemark"
label="备注"
prop="remark"
>
<ElInput
v-model="formData.remark"
type="textarea"
:rows="3"
placeholder="请输入备注信息(可选)"
/>
</ElFormItem>
<!-- 选中的网卡信息展示 -->
<ElFormItem
v-if="selectedCards.length > 0"
label="选中网卡"
>
<div class="selected-cards-info">
<ElTag type="info">已选择 {{ selectedCards.length }} 张网卡</ElTag>
<ElButton
type="text"
size="small"
@click="showCardList = !showCardList"
>
{{ showCardList ? '收起' : '查看详情' }}
</ElButton>
</div>
<div v-if="showCardList" class="card-list">
<div
v-for="card in selectedCards.slice(0, 5)"
:key="card.id"
class="card-item"
>
{{ card.iccid }}
</div>
<div v-if="selectedCards.length > 5" class="more-cards">
还有 {{ selectedCards.length - 5 }} 张网卡...
</div>
</div>
</ElFormItem>
</ElForm>
<template #footer>
<ElButton @click="handleClose">取消</ElButton>
<ElButton type="primary" @click="handleConfirm" :loading="confirmLoading">
确认
</ElButton>
</template>
</ElDialog>
</template>
<script setup lang="ts">
import { ElDialog, ElForm, ElFormItem, ElSelect, ElOption, ElInput, ElButton, ElTag, ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
interface SelectOption {
label: string
value: string | number
}
interface SelectedCard {
id: number | string
iccid: string
[key: string]: any
}
interface Props {
// 弹框基础配置
title: string
width?: string
// 下拉选择配置
showSelect?: boolean
selectLabel?: string
selectProp?: string
selectPlaceholder?: string
// 其他表单项配置
showAmount?: boolean
showRemark?: boolean
// 选中的网卡
selectedCards?: SelectedCard[]
// 远程搜索方法
remoteSearch?: (query: string) => Promise<SelectOption[]>
}
interface Emits {
(e: 'update:visible', value: boolean): void
(e: 'confirm', data: any): void
(e: 'close'): void
}
const props = withDefaults(defineProps<Props>(), {
width: '500px',
showSelect: true,
selectLabel: '请选择',
selectProp: 'selectedValue',
selectPlaceholder: '请选择选项',
showAmount: false,
showRemark: true,
selectedCards: () => []
})
const emit = defineEmits<Emits>()
const visible = defineModel<boolean>('visible', { default: false })
const formRef = ref<FormInstance>()
const selectLoading = ref(false)
const confirmLoading = ref(false)
const showCardList = ref(false)
// 表单数据
const formData = reactive({
selectedValue: '',
amount: '',
remark: ''
})
// 下拉选项
const selectOptions = ref<SelectOption[]>([])
// 表单验证规则
const formRules = computed<FormRules>(() => {
const rules: FormRules = {}
if (props.showSelect) {
rules[props.selectProp!] = [
{ required: true, message: `请选择${props.selectLabel}`, trigger: 'change' }
]
}
if (props.showAmount) {
rules.amount = [
{ required: true, message: '请输入充值金额', trigger: 'blur' },
{ pattern: /^\d+(\.\d{1,2})?$/, message: '请输入正确的金额格式', trigger: 'blur' }
]
}
return rules
})
// 处理远程搜索
const handleRemoteSearch = async (query: string) => {
if (!props.remoteSearch) return
selectLoading.value = true
try {
const options = await props.remoteSearch(query)
selectOptions.value = options
} catch (error) {
console.error('远程搜索失败:', error)
ElMessage.error('搜索失败,请重试')
} finally {
selectLoading.value = false
}
}
// 初始化时加载默认选项
const loadDefaultOptions = async () => {
if (props.remoteSearch) {
// 加载默认数据传入空字符串以获取默认的10条数据
await handleRemoteSearch('')
}
}
// 处理确认
const handleConfirm = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
confirmLoading.value = true
// 发送表单数据
const submitData = {
...formData,
selectedCards: props.selectedCards
}
emit('confirm', submitData)
} catch (error) {
console.error('表单验证失败:', error)
} finally {
confirmLoading.value = false
}
}
// 处理关闭
const handleClose = () => {
formRef.value?.resetFields()
showCardList.value = false
emit('close')
visible.value = false
}
// 监听弹框显示状态
watch(visible, (newVal) => {
if (newVal) {
loadDefaultOptions()
}
})
</script>
<style lang="scss" scoped>
.selected-cards-info {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
}
.card-list {
margin-top: 8px;
padding: 8px;
background-color: var(--el-fill-color-lighter);
border-radius: 4px;
max-height: 120px;
overflow-y: auto;
.card-item {
padding: 2px 0;
font-size: 12px;
color: var(--el-text-color-regular);
}
.more-cards {
padding: 2px 0;
font-size: 12px;
color: var(--el-text-color-placeholder);
font-style: italic;
}
}
</style>

View File

@@ -0,0 +1,25 @@
<template>
<el-tag :type="tagType" :effect="effect" :size="size">
{{ statusLabel }}
</el-tag>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { getCardStatusLabel, getCardStatusType } from '@/config/constants'
import type { CardStatus } from '@/types/api'
interface Props {
status: CardStatus
effect?: 'dark' | 'light' | 'plain'
size?: 'large' | 'default' | 'small'
}
const props = withDefaults(defineProps<Props>(), {
effect: 'light',
size: 'default'
})
const statusLabel = computed(() => getCardStatusLabel(props.status))
const tagType = computed(() => getCardStatusType(props.status))
</script>

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