first commit

This commit is contained in:
2026-06-03 10:09:03 +08:00
commit 81f5039458
6266 changed files with 188236 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
output
dist-ssr
*.local
output.zip
dist.zip
dist.7z
output.7z
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.env
.env.*
!.example.env
+5
View File
@@ -0,0 +1,5 @@
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
+11
View File
@@ -0,0 +1,11 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const ElMessage: typeof import('element-plus/es').ElMessage
const ElMessageBox: typeof import('element-plus/es').ElMessageBox
}
+76
View File
@@ -0,0 +1,76 @@
/* eslint-disable */
// @ts-nocheck
// biome-ignore lint: disable
// oxlint-disable
// ------
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
CommonAside: typeof import('./src/components/CommonAside.vue')['default']
CommonHeader: typeof import('./src/components/CommonHeader.vue')['default']
ElAlert: typeof import('element-plus/es')['ElAlert']
ElAside: typeof import('element-plus/es')['ElAside']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElBacktop: typeof import('element-plus/es')['ElBacktop']
ElButton: typeof import('element-plus/es')['ElButton']
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
ElCard: typeof import('element-plus/es')['ElCard']
ElCascader: typeof import('element-plus/es')['ElCascader']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
ElCol: typeof import('element-plus/es')['ElCol']
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
ElContainer: typeof import('element-plus/es')['ElContainer']
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']
ElHeader: typeof import('element-plus/es')['ElHeader']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElImage: typeof import('element-plus/es')['ElImage']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElLink: typeof import('element-plus/es')['ElLink']
ElMain: typeof import('element-plus/es')['ElMain']
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']
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']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElTree: typeof import('element-plus/es')['ElTree']
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
ElUpload: typeof import('element-plus/es')['ElUpload']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
export interface GlobalDirectives {
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
}
}
+295
View File
@@ -0,0 +1,295 @@
# 七牛云上传功能文档索引
## 快速开始
如果你想快速测试七牛云上传功能,请按以下顺序阅读:
1. **[七牛云上传测试步骤](./七牛云上传测试步骤.md)** ⭐ 推荐先看
- 测试前的准备工作
- 详细的测试步骤
- 常见问题排查
2. **[七牛云区域配置修复说明](./七牛云区域配置修复说明.md)**
- 了解最近修复的区域配置问题
- 查看修复的具体内容
3. **[七牛云直传配置](./七牛云直传配置.md)**
- 完整的技术文档
- API 接口说明
- 代码使用示例
4. **[七牛云区域配置流程图](./七牛云区域配置流程图.md)**
- 可视化流程图
- 帮助理解上传机制
## 文档说明
### 七牛云上传测试步骤.md
**适合人群**: 测试人员、开发人员
**内容**:
- 测试前的准备工作
- 详细的测试步骤
- 验证方法
- 常见问题排查
- 性能测试指南
**何时阅读**: 准备测试上传功能时
---
### 七牛云区域配置修复说明.md
**适合人群**: 开发人员、运维人员
**内容**:
- 问题描述和原因分析
- 解决方案详解
- 修复的文件列表
- 验证步骤
- 区域切换方法
**何时阅读**: 遇到区域配置问题或想了解修复细节时
---
### 七牛云直传配置.md
**适合人群**: 开发人员
**内容**:
- 上传流程对比(旧 vs 新)
- 安装依赖
- 后端 API 文档
- 前端使用示例
- 工作原理
- 安全性说明
- 性能优化
- 故障排查
- 迁移指南
**何时阅读**: 需要了解完整技术实现或集成到其他页面时
---
### 七牛云区域配置流程图.md
**适合人群**: 所有人
**内容**:
- 问题和解决方案的可视化对比
- 完整上传流程图
- 区域映射关系图
- 代码对比
- 优势对比
**何时阅读**: 想快速理解上传机制时
---
## 最近更新 (2026-04-09)
### ✅ 已完成
- 修复了七牛云区域配置问题
- 添加了 `getQiniuRegion()` 函数
- 实现了动态区域配置
- 创建了完整的测试文档
### 🔧 修复的问题
**问题**: 上传时出现 "incorrect region" 错误
**原因**: 前端硬编码区域为 z0(华东),但数据库配置为 z2(华南)
**解决**: 前端动态从后端获取区域配置,自动使用正确的区域
### 📝 涉及的文件
- `platform/src/utils/qiniuUpload.js` - 添加区域映射函数
- `go/controllers/qiniu_upload.go` - 返回区域配置
- `platform/docs/*.md` - 创建文档
---
## 技术架构
```
┌─────────────────────────────────────────────────────────┐
│ 七牛云直传架构 │
└─────────────────────────────────────────────────────────┘
前端 (Vue3)
├── platform/src/utils/qiniuUpload.js
│ ├── smartUpload() - 智能选择上传方式
│ ├── uploadToQiniu() - 七牛云直传
│ ├── uploadToLocal() - 本地上传(中转)
│ ├── getStorageConfig() - 获取存储配置
│ ├── getQiniuToken() - 获取上传凭证
│ ├── saveFileRecord() - 保存文件记录
│ ├── batchUpload() - 批量上传
│ └── getQiniuRegion() - 区域映射 ⭐ 新增
└── platform/src/views/platform/softwareupgrade/components/edit.vue
└── 使用 smartUpload() 上传文件
后端 (Go + Beego)
├── go/controllers/qiniu_upload.go
│ ├── GetStorageConfig() - 获取存储配置
│ ├── GetUploadToken() - 生成上传凭证
│ ├── SaveFileRecord() - 保存文件记录
│ └── getQiniuUploadURL() - 获取上传地址
├── go/routers/platform/platform.go
│ ├── GET /platform/storage/config
│ ├── GET /platform/qiniu/token
│ └── POST /platform/qiniu/save
└── go/models/storage_config.go
└── GetStorageConfig() - 从数据库读取配置
数据库 (MySQL)
├── system_storage_config - 存储配置表
│ ├── storage_type - 存储类型 (local/qiniu)
│ ├── qiniu_region - 七牛云区域 (z0/z1/z2/...)
│ ├── qiniu_bucket - 存储空间名称
│ ├── qiniu_domain - 访问域名
│ ├── qiniu_access_key - AccessKey
│ └── qiniu_secret_key - SecretKey
└── system_file - 文件记录表
├── id - 文件 ID
├── name - 文件名
├── src - 文件 URL
├── size - 文件大小
├── md5 - 文件 MD5
└── ...
七牛云 (Qiniu Cloud)
└── 存储空间: yunzerwebsite
├── 区域: z2 (华南)
├── 域名: http://7colud.yunzer.cn
└── 上传地址: https://up-z2.qiniup.com
```
---
## 上传流程
### 简化流程
```
1. 用户选择文件
2. 前端获取存储配置 (storageType: 'qiniu')
3. 前端获取上传凭证 (token, region: 'z2')
4. 前端直接上传到七牛云 (up-z2.qiniup.com)
5. 前端保存文件记录到数据库
6. 显示上传成功
```
### 详细流程
参见 [七牛云区域配置流程图.md](./七牛云区域配置流程图.md)
---
## 支持的区域
| 区域代码 | 区域名称 | 上传地址 | 状态 |
|---------|---------|---------|------|
| z0 | 华东 | up-z0.qiniup.com | ✓ 支持 |
| z1 | 华北 | up-z1.qiniup.com | ✓ 支持 |
| z2 | 华南 | up-z2.qiniup.com | ✓ 支持 (当前) |
| na0 | 北美 | up-na0.qiniup.com | ✓ 支持 |
| as0 | 新加坡 | up-as0.qiniup.com | ✓ 支持 |
| cn-east-2 | 华东-浙江2 | up-cn-east-2.qiniup.com | ✓ 支持 |
---
## 常见问题
### Q1: 如何测试上传功能?
**A**: 参见 [七牛云上传测试步骤.md](./七牛云上传测试步骤.md)
### Q2: 上传时出现 "incorrect region" 错误?
**A**: 已修复。参见 [七牛云区域配置修复说明.md](./七牛云区域配置修复说明.md)
### Q3: 如何切换到其他区域?
**A**: 修改数据库配置即可,无需改代码:
```sql
UPDATE system_storage_config
SET qiniu_region = 'z1' -- 切换到华北
WHERE id = 1;
```
### Q4: 如何在其他页面使用上传功能?
**A**: 参见 [七牛云直传配置.md](./七牛云直传配置.md) 的"前端使用"章节
### Q5: 大文件上传会超时吗?
**A**: 不会。七牛云 SDK 支持:
- 自动分片上传
- 断点续传
- 并发上传
- 前端已设置 `timeout: 0`(无超时限制)
### Q6: 上传是否经过服务器?
**A**: 不经过。前端直接上传到七牛云,服务器只负责:
- 生成上传凭证
- 保存文件记录到数据库
### Q7: 如何回滚到本地存储?
**A**: 修改数据库配置:
```sql
UPDATE system_storage_config
SET storage_type = 'local'
WHERE id = 1;
```
前端会自动切换到本地上传方式。
---
## 性能对比
### 旧方案(服务器中转)
```
100MB 文件上传时间:
- 上传到服务器:2 分钟
- 服务器上传到七牛云:2 分钟
- 总计:4 分钟
```
### 新方案(直传)
```
100MB 文件上传时间:
- 直接上传到七牛云:2 分钟
- 总计:2 分钟
```
**效率提升**: 50%
---
## 依赖
### 前端
- `qiniu-js@^3.4.4` - 七牛云 JavaScript SDK
### 后端
- `github.com/qiniu/go-sdk/v7@v7.18.2` - 七牛云 Go SDK
---
## 相关链接
- [七牛云官方文档](https://developer.qiniu.com/)
- [七牛云存储区域](https://developer.qiniu.com/kodo/1671/region-endpoint-fq)
- [qiniu-js SDK 文档](https://developer.qiniu.com/kodo/1283/javascript)
---
## 联系方式
如有问题,请联系开发团队。
---
## 更新日期
2026-04-09
+82
View File
@@ -0,0 +1,82 @@
# Platform前端项目文档
## 📚 文档目录
### 功能使用文档
- [字典使用说明](./dictionary-usage.md)
- [Pinia字典指南](./pinia-dict-guide.md)
- [调用字典](./调用字典.md)
- [接口调用](./接口调用.md)
- [拼接接口路径](./拼接接口路径.md)
- [获取缓存数据](./获取缓存数据.md)
- [调用图片上传组件](./调用图片上传组件.md)
- [一键复制](./一键复制.md)
- [文件上传超时配置](./文件上传超时配置.md) - 大文件上传配置说明
- [图片URL处理说明](./图片URL处理说明.md) - 图片URL拼接处理
- [七牛云直传配置](./七牛云直传配置.md) - ⚡ 七牛云直传上传(推荐!)
### 存储配置功能
存储配置功能的前端实现已完成,包括:
- 存储配置界面组件:`src/views/system/platformsettings/components/storageSettings.vue`
- API接口:`src/api/sitesettings.js`
详细文档请查看后端项目文档:`go/docs/README_STORAGE.md`
## 🚀 快速导航
### 新手入门
1. 了解项目结构
2. 阅读 [接口调用](./接口调用.md)
3. 学习 [字典使用](./dictionary-usage.md)
### 常用功能
- 字典管理:[dictionary-usage.md](./dictionary-usage.md)
- 图片上传:[调用图片上传组件.md](./调用图片上传组件.md)
- 数据缓存:[获取缓存数据.md](./获取缓存数据.md)
## 📂 项目结构
```
platform/
├── src/
│ ├── api/ # API接口
│ ├── assets/ # 静态资源
│ ├── components/ # 公共组件
│ ├── router/ # 路由配置
│ ├── stores/ # 状态管理
│ ├── utils/ # 工具函数
│ └── views/ # 页面组件
├── public/ # 公共资源
├── docs/ # 文档(本目录)
└── package.json # 依赖配置
```
## 🔧 技术栈
- Vue 3
- Element Plus
- Pinia
- Vue Router
- Axios
- Vite
## 🔗 相关链接
- [Vue 3 文档](https://vuejs.org/)
- [Element Plus 文档](https://element-plus.org/)
- [Pinia 文档](https://pinia.vuejs.org/)
- [Vite 文档](https://vitejs.dev/)
## 📝 更新日志
### 2026-04-09
- ✅ 移除文件上传超时限制
- ✅ 支持大文件上传(配合后端 2GB 限制)
- ✅ 修复图片 URL 重复拼接问题
- ✅ 完善文件上传文档
### 2024-01-01
- ✅ 完成存储配置界面
- ✅ 支持本地存储和七牛云存储配置
- ✅ 表单验证和草稿保存
- ✅ 完善文档体系
+1
View File
@@ -0,0 +1 @@
# 字典使用示例
+393
View File
@@ -0,0 +1,393 @@
# Pinia 字典管理系统使用指南
## 系统架构
```
┌─────────────────────────────────────┐
│ API 接口 (getDictItemsByCode) │
│ /api/dict/items/code/{code} │
└──────────────┬──────────────────────┘
┌─────────────────────────────────────┐
│ Pinia Store (useDictStore) │
│ ✅ 自动缓存字典数据 │
│ ✅ 避免重复请求 │
│ ✅ 支持同步/异步访问 │
└──────────────┬──────────────────────┘
┌──────┴──────┐
↓ ↓
┌────────┐ ┌──────────────┐
│组件 │ │Composable │
│直接用 │ │useDict Hook │
└────────┘ └──────────────┘
```
---
## 核心文件说明
### 1. **Store**: `src/stores/dict.js`
字典数据的全局管理器
**主要方法**
```javascript
import { useDictStore } from '@/stores/dict'
const dictStore = useDictStore()
// ✅ 异步获取字典(推荐)
const items = await dictStore.getDictItems('user_status')
// ✅ 同步获取字典(已缓存时)
const items = dictStore.getDictItemsSync('user_status')
// ✅ 获取字典值对应的标签
const label = dictStore.getDictLabel('user_status', 1)
// ✅ 预加载多个字典
await dictStore.preloadDicts(['user_status', 'user_role'])
// ✅ 清空缓存
dictStore.clearCache('user_status')
```
---
### 2. **常量**: `src/constants/dictCodes.js`
集中管理所有字典编码
**使用示例**
```javascript
import { DICT_CODES } from '@/constants/dictCodes'
// 好处:避免硬编码,IDE 有自动完成
const items = await dictStore.getDictItems(DICT_CODES.USER_STATUS)
// 所有可用的编码:
DICT_CODES.USER_STATUS // 用户状态
DICT_CODES.USER_GENDER // 用户性别
DICT_CODES.USER_ROLE // 用户角色
DICT_CODES.DEPT_STATUS // 部门状态
DICT_CODES.POSITION_STATUS // 职位状态
// ... 更多编码
```
---
### 3. **Composable**: `src/composables/useDict.js`
简化在组件中使用字典的 Hook
**基础用法**
```javascript
import { useDictionary, useUserStatusDict } from '@/composables/useDict'
import { DICT_CODES } from '@/constants/dictCodes'
// 方式1:使用常量
const { statusDict, loading } = useDictionary(DICT_CODES.USER_STATUS)
// 方式2:使用字符串
const { dicts, loading } = useDictionary('user_status')
// 方式3:使用特化 Hook(推荐)
const { user_statusDict, loading } = useUserStatusDict()
```
---
## 使用场景
### 场景1:在列表页加载字典
**文件**: `src/views/system/users/index.vue`
```vue
<script setup>
import { useDictStore } from '@/stores/dict'
import { ref, onMounted } from 'vue'
const dictStore = useDictStore()
const statusDict = ref([])
const fetchStatusDict = async () => {
statusDict.value = await dictStore.getDictItems('user_status')
}
onMounted(async () => {
await fetchStatusDict()
})
</script>
<template>
<!-- 传给子组件 -->
<UserEditDialog :status-dict="statusDict" />
</template>
```
---
### 场景2:在编辑对话框使用字典
**文件**: `src/views/system/users/components/UserEdit.vue`
```vue
<template>
<!-- 状态选择 -->
<el-form-item label="状态">
<el-select v-model="form.status">
<el-option
v-for="item in statusDict"
:key="item.dict_value"
:label="item.dict_label"
:value="item.dict_value"
/>
</el-select>
</el-form-item>
</template>
<script setup>
const props = defineProps({
statusDict: {
type: Array,
default: () => [],
},
})
</script>
```
---
### 场景3:快速使用 Composable Hook
最简单的方式,自动处理加载:
```vue
<template>
<div>
<p v-if="loading">加载中...</p>
<el-select v-else v-model="status">
<el-option
v-for="item in user_statusDict"
:key="item.dict_value"
:label="item.dict_label"
:value="item.dict_value"
/>
</el-select>
</div>
</template>
<script setup>
import { useUserStatusDict } from '@/composables/useDict'
import { ref } from 'vue'
const status = ref('active')
const { user_statusDict, loading } = useUserStatusDict()
</script>
```
---
### 场景4:预加载应用启动时需要的字典
**文件**: `src/main.js`
```javascript
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { useDictStore } from '@/stores/dict'
import { DICT_CODES } from '@/constants/dictCodes'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
// 在应用启动后预加载常用字典
const dictStore = useDictStore()
await dictStore.preloadDicts([
DICT_CODES.USER_STATUS,
DICT_CODES.COMMON_STATUS,
DICT_CODES.YES_NO,
])
app.mount('#app')
```
---
## 字典数据结构
后端返回的字典数据结构:
```json
[
{
"dict_id": 1,
"dict_value": "1",
"dict_label": "启用",
"dict_type": "user_status",
"remarks": "用户启用状态",
"remark": "用户启用状态"
},
{
"dict_id": 2,
"dict_value": "0",
"dict_label": "禁用",
"dict_type": "user_status",
"remarks": "用户禁用状态",
"remark": "用户禁用状态"
}
]
```
**关键字段**
- `dict_value`: 字典值(存储在数据库中)
- `dict_label`: 字典标签(显示给用户)
- `dict_type`: 字典类型编码(如 'user_status'
---
## 最佳实践
### ✅ DO
1. **使用常量而不是硬编码字符串**
```javascript
// ✅ 好
dictStore.getDictItems(DICT_CODES.USER_STATUS)
// ❌ 差
dictStore.getDictItems('user_status')
```
2. **在父组件加载,通过 props 传给子组件**
```javascript
// ✅ 父组件负责数据,子组件负责展示
// index.vue
const statusDict = await dictStore.getDictItems(DICT_CODES.USER_STATUS)
// UserEdit.vue
const props = defineProps({ statusDict: Array })
```
3. **用 Composable 简化组件逻辑**
```javascript
// ✅ 一行代码搞定
const { user_statusDict, loading } = useUserStatusDict()
```
4. **预加载常用字典**
```javascript
// ✅ 应用启动时预加载,避免页面初始化时加载
await dictStore.preloadDicts([...])
```
### ❌ DON'T
1. **不要在多个地方重复加载同一个字典**
```javascript
// ❌ 糟糕:重复加载
// 页面A
const dict1 = await dictStore.getDictItems('user_status')
// 页面B(Store 会自动缓存,但代码看起来重复)
const dict2 = await dictStore.getDictItems('user_status')
```
2. **不要忘记处理加载状态**
```javascript
// ❌ 可能展示空白
const { statusDict } = useUserStatusDict()
// ✅ 处理加载状态
const { statusDict, loading } = useUserStatusDict()
if (loading) { /* 显示加载中 */ }
```
3. **不要混用不同的字典访问方式**
```javascript
// ❌ 混乱
const dict1 = await dictStore.getDictItems('user_status')
const dict2 = dictStore.getDictItemsSync('user_role')
// ✅ 统一使用
const dict1 = await dictStore.getDictItems('user_status')
const dict2 = await dictStore.getDictItems('user_role')
```
---
## 性能优化建议
| 优化项 | 说明 |
|------|------|
| **缓存** | Store 自动缓存,同一个字典只请求一次 |
| **预加载** | 在路由切换前预加载需要的字典 |
| **同步访问** | 已加载的字典可用 `getDictItemsSync` 同步获取 |
| **避免重复** | 不要在多个组件重复请求同一个字典 |
---
## 故障排查
### 问题1:状态选项为空
**原因**:字典未加载
**解决**
```javascript
// ❌ 错误:字典还未加载
const statusDict = dictStore.getDictItemsSync('user_status') // 返回 []
// ✅ 正确:等待异步加载完成
const statusDict = await dictStore.getDictItems('user_status')
```
### 问题2:重复加载字典
**原因**:没有使用 Store 的缓存
**解决**
```javascript
// 所有调用都会自动使用缓存,只请求一次
await dictStore.getDictItems('user_status') // 首次:发送请求
await dictStore.getDictItems('user_status') // 第二次:返回缓存
```
### 问题3:字典显示不对
**原因**value 类型不匹配(如 1 vs "1"
**解决**
```javascript
// Store 会自动处理类型匹配
const item = items.find(i =>
String(i.dict_value) === String(value) || i.dict_value === value
)
```
---
## 集成检清表
- [ ] 创建 `src/stores/dict.js` - Store
- [ ] 创建 `src/constants/dictCodes.js` - 常量
- [ ] 创建 `src/composables/useDict.js` - Composable
- [ ] 在 `index.vue` 中导入 `useDictStore`
- [ ] 在 `UserEdit.vue` 中接收 `statusDict` props
- [ ] 测试字典加载和显示
- [ ] 验证缓存功能(打开浏览器 DevTools 检查 Network
- [ ] 预加载常用字典(可选)
---
## 相关文件修改
已修改的文件:
- ✅ `src/stores/dict.js` - 新建
- ✅ `src/constants/dictCodes.js` - 新建
- ✅ `src/composables/useDict.js` - 新建
- ✅ `src/views/system/users/index.vue` - 使用 `useDictStore`
- ✅ `src/views/system/users/components/UserEdit.vue` - 导入字典库
+31
View File
@@ -0,0 +1,31 @@
<template>
<el-button @click="handleCopy">复制数据</el-button>
</template>
<script setup lang='ts'>
import { ElMessage } from 'element-plus'
//一键复制代码
function handleCopy() {
if (!props.model) return
const data = [
`客户名称: ${props.model.name || '-'}`,
`联系人: ${props.model.contact || '-'}`,
`电话: ${props.model.phone || '-'}`,
`邮箱: ${props.model.email || '-'}`,
`客户类型: ${getLabel(customerTypeOptions.value, props.model.customer_type)}`,
`客户等级: ${getLabel(customerLevelOptions.value, props.model.customer_level)}`,
`所属行业: ${getLabel(industryOptions.value, props.model.industry)}`,
`客户状态: ${getLabel(customerStatusOptions.value, String(props.model.status ?? '')) || ((props.model.status===1||props.model.status==='1') ? '正常' : '停用')}`,
`地址: ${props.model.address || '-'}`,
`经营范围: ${props.model.business_scope || '-'}`
].join('\n')
navigator.clipboard.writeText(data).then(() => {
ElMessage.success('数据已复制到剪贴板')
}).catch(() => {
ElMessage.error('复制失败')
})
}
</script>
@@ -0,0 +1,201 @@
# 七牛云上传测试步骤
## 前置条件
1. 数据库配置已正确设置:
- `storage_type = 'qiniu'`
- `qiniu_region = 'z2'` (华南)
- `qiniu_bucket = 'yunzerwebsite'`
- `qiniu_domain = 'http://7colud.yunzer.cn'`
- `qiniu_access_key``qiniu_secret_key` 已配置
2. 前端依赖已安装:
- `qiniu-js@^3.4.4` ✓ (已在 package.json 中)
3. 后端服务已重启:
```bash
systemctl restart go-api
```
## 测试步骤
### 1. 登录系统
访问前端页面并登录:
- URL: https://platform.yunzer.cn
- 账号: hero920103
- 密码: 920103
### 2. 进入软件升级页面
导航到:平台管理 → 软件升级
### 3. 上传文件测试
点击"新增"或"编辑"按钮,在弹出的对话框中:
1. 点击"上传软件包"按钮
2. 选择一个文件(建议先用小文件测试,如 1-10MB)
3. 观察上传进度条
4. 等待上传完成
### 4. 验证上传结果
#### 前端验证
- 检查是否显示上传成功消息
- 检查文件 URL 是否正确(应该是 `http://7colud.yunzer.cn/...`
- 检查文件是否可以预览/下载
#### 后端日志验证
```bash
tail -f /www/wwwroot/api.yunzer.cn/go.log
```
查看日志中是否有:
- `GET /platform/storage/config` - 获取存储配置
- `GET /platform/qiniu/token` - 获取上传凭证
- `POST /platform/qiniu/save` - 保存文件记录
#### 七牛云控制台验证
1. 登录七牛云控制台
2. 进入 `yunzerwebsite` 存储空间
3. 检查文件是否已上传
4. 文件路径格式应为:`2026/04/09/时间戳.扩展名`
#### 数据库验证
```sql
SELECT id, name, src, size, type, cate, md5, create_time
FROM system_file
ORDER BY id DESC
LIMIT 5;
```
检查:
- `src` 字段应该是完整的七牛云 URL
- `md5` 字段应该有值
- `size` 字段应该正确
## 预期结果
### 成功标志
1. 前端显示上传成功
2. 文件 URL 格式正确:`http://7colud.yunzer.cn/2026/04/09/xxxxx.ext`
3. 七牛云控制台能看到文件
4. 数据库有对应记录
5. 文件可以正常访问和下载
### 上传流程
```
前端 → 获取存储配置 (storageType: 'qiniu')
→ 获取上传凭证 (token, region: 'z2')
→ 直接上传到七牛云 (up-z2.qiniup.com)
→ 保存文件记录到数据库
→ 返回文件 URL
```
## 常见问题排查
### 问题 1: "incorrect region" 错误
**错误信息**:
```
xhr request failed, code: 400
response: {"error":"incorrect region, please use up-z2.qiniup.com, bucket is: yunzerwebsite"}
```
**原因**: 前端使用的区域与七牛云 bucket 实际区域不匹配
**解决**:
- 已修复:添加了 `getQiniuRegion()` 函数,根据后端返回的 region 动态设置
- 后端返回 `region: 'z2'`,前端会自动使用 `qiniu.region.z2`
### 问题 2: "获取上传凭证失败"
**可能原因**:
- 未登录或 token 过期
- 存储配置未设置为七牛云
- 七牛云配置不完整
**解决**:
1. 检查登录状态
2. 检查数据库 `system_storage_config` 表
3. 确认 AccessKey 和 SecretKey 正确
### 问题 3: 上传后文件无法访问
**可能原因**:
- 七牛云域名配置错误
- 域名未绑定或未备案
- 文件权限设置问题
**解决**:
1. 检查 `qiniu_domain` 配置
2. 登录七牛云控制台检查域名绑定
3. 检查存储空间访问权限(应为公开)
### 问题 4: 大文件上传失败
**可能原因**:
- 网络超时
- 浏览器限制
**解决**:
1. 七牛云 SDK 支持断点续传,会自动重试
2. 检查网络连接
3. 尝试分片上传(SDK 自动处理)
## 性能测试
### 小文件测试 (< 10MB)
- 预期时间:几秒内完成
- 不会分片上传
### 中等文件测试 (10MB - 100MB)
- 预期时间:根据网速,通常 1-2 分钟
- 可能会分片上传
### 大文件测试 (> 100MB)
- 预期时间:根据网速
- 会自动分片上传
- 支持断点续传
## 对比测试
### 旧方案(服务器中转)
```
100MB 文件上传时间:
- 上传到服务器:2 分钟
- 服务器上传到七牛云:2 分钟
- 总计:4 分钟
```
### 新方案(直传)
```
100MB 文件上传时间:
- 直接上传到七牛云:2 分钟
- 总计:2 分钟
```
**效率提升**: 50%
## 回滚方案
如果七牛云直传有问题,可以临时切换回本地存储:
```sql
UPDATE system_storage_config
SET storage_type = 'local'
WHERE id = 1;
```
前端会自动检测并使用本地上传方式(通过服务器中转)。
## 相关文件
- `platform/src/utils/qiniuUpload.js` - 上传工具(已添加 getQiniuRegion 函数)
- `go/controllers/qiniu_upload.go` - 后端控制器
- `platform/docs/七牛云直传配置.md` - 详细配置文档
## 更新日期
2026-04-09
@@ -0,0 +1,248 @@
# 七牛云区域配置修复说明
## 问题描述
上传文件到七牛云时出现区域错误:
```
xhr request failed, code: 400
response: {"error":"incorrect region, please use up-z2.qiniup.com, bucket is: yunzerwebsite"}
```
## 问题原因
前端代码中硬编码了七牛云区域为 `z0`(华东),但实际数据库配置的是 `z2`(华南):
```javascript
// 错误的硬编码方式
const config = {
useCdnDomain: true,
region: qiniu.region.z0, // ❌ 硬编码为华东
};
```
数据库实际配置:
- `qiniu_region = 'z2'` (华南)
- `qiniu_bucket = 'yunzerwebsite'`
## 解决方案
### 1. 添加区域映射函数
`platform/src/utils/qiniuUpload.js` 文件末尾添加了 `getQiniuRegion()` 函数:
```javascript
/**
* 根据区域代码获取七牛云区域对象
* @param {string} regionCode 区域代码 (z0, z1, z2, na0, as0, cn-east-2)
* @returns {Object} 七牛云区域对象
*/
function getQiniuRegion(regionCode) {
switch (regionCode) {
case 'z0':
return qiniu.region.z0; // 华东
case 'z1':
return qiniu.region.z1; // 华北
case 'z2':
return qiniu.region.z2; // 华南
case 'na0':
return qiniu.region.na0; // 北美
case 'as0':
return qiniu.region.as0; // 新加坡
case 'cn-east-2':
return qiniu.region.cnEast2; // 华东-浙江2
default:
return qiniu.region.z0; // 默认华东
}
}
```
### 2. 动态获取区域配置
修改 `uploadToQiniu()` 函数,从后端返回的数据中读取区域配置:
```javascript
export async function uploadToQiniu(file, options = {}) {
// 1. 获取上传凭证(包含区域信息)
const tokenRes = await getQiniuToken();
const { token, keyPrefix, domain, region, uploadUrl } = tokenRes.data;
// 2. 根据后端返回的区域代码动态获取区域对象
const qiniuRegion = getQiniuRegion(region); // ✓ 动态获取
const config = {
useCdnDomain: true,
region: qiniuRegion, // ✓ 使用正确的区域
};
// 3. 上传文件
const observable = qiniu.upload(file, key, token, putExtra, config);
// ...
}
```
## 工作流程
```
1. 前端调用 getQiniuToken()
2. 后端从数据库读取配置
- storage_type: 'qiniu'
- qiniu_region: 'z2'
- qiniu_bucket: 'yunzerwebsite'
3. 后端返回配置信息
{
token: "...",
region: "z2", ← 区域代码
bucket: "yunzerwebsite",
uploadUrl: "https://up-z2.qiniup.com"
}
4. 前端调用 getQiniuRegion('z2')
返回: qiniu.region.z2
5. 使用正确的区域上传文件
上传地址: https://up-z2.qiniup.com
```
## 支持的区域
| 区域代码 | 区域名称 | qiniu-js 对象 | 上传地址 |
|---------|---------|--------------|---------|
| z0 | 华东 | qiniu.region.z0 | https://up-z0.qiniup.com |
| z1 | 华北 | qiniu.region.z1 | https://up-z1.qiniup.com |
| z2 | 华南 | qiniu.region.z2 | https://up-z2.qiniup.com |
| na0 | 北美 | qiniu.region.na0 | https://up-na0.qiniup.com |
| as0 | 新加坡 | qiniu.region.as0 | https://up-as0.qiniup.com |
| cn-east-2 | 华东-浙江2 | qiniu.region.cnEast2 | https://up-cn-east-2.qiniup.com |
## 验证步骤
### 1. 确认依赖已安装
```bash
cd platform
npm list qiniu-js
```
预期输出:
```
qiniu-js@3.4.4
```
✓ 已确认:`qiniu-js@^3.4.4` 已在 `package.json`
### 2. 重启后端服务
```bash
systemctl restart go-api
systemctl status go-api
```
### 3. 测试上传
1. 登录系统
2. 进入软件升级页面
3. 上传一个文件
4. 观察是否成功
### 4. 检查日志
```bash
# 后端日志
tail -f /www/wwwroot/api.yunzer.cn/go.log
# 前端控制台
# 打开浏览器开发者工具,查看 Network 标签
```
预期请求:
```
GET /platform/storage/config
→ { storageType: 'qiniu', qiniuRegion: 'z2' }
GET /platform/qiniu/token
→ { token: '...', region: 'z2', uploadUrl: 'https://up-z2.qiniup.com' }
POST https://up-z2.qiniup.com ← 直接上传到七牛云
→ { key: '...', hash: '...', size: ... }
POST /platform/qiniu/save
→ { url: 'http://7colud.yunzer.cn/...', id: 123 }
```
## 修复的文件
### 前端
- `platform/src/utils/qiniuUpload.js`
- 添加了 `getQiniuRegion()` 函数
- 修改了 `uploadToQiniu()` 函数,使用动态区域配置
### 后端
- `go/controllers/qiniu_upload.go`
- `GetUploadToken()` 方法返回 `region` 字段
- `getQiniuUploadURL()` 函数根据区域返回正确的上传地址
## 优势
### 1. 灵活性
- 支持所有七牛云区域
- 无需修改代码即可切换区域
- 只需在数据库中修改配置
### 2. 可维护性
- 区域配置集中管理
- 代码更清晰易懂
- 便于扩展新区域
### 3. 正确性
- 自动使用正确的上传地址
- 避免区域不匹配错误
- 提高上传成功率
## 切换区域方法
如果需要切换到其他区域,只需修改数据库配置:
```sql
-- 切换到华东 (z0)
UPDATE system_storage_config
SET qiniu_region = 'z0'
WHERE id = 1;
-- 切换到华北 (z1)
UPDATE system_storage_config
SET qiniu_region = 'z1'
WHERE id = 1;
-- 切换到华南 (z2) - 当前配置
UPDATE system_storage_config
SET qiniu_region = 'z2'
WHERE id = 1;
```
前端会自动使用新的区域配置,无需重启或修改代码。
## 相关文档
- [七牛云直传配置说明](./七牛云直传配置.md)
- [七牛云上传测试步骤](./七牛云上传测试步骤.md)
- [七牛云官方文档 - 存储区域](https://developer.qiniu.com/kodo/1671/region-endpoint-fq)
## 更新日期
2026-04-09
## 状态
✅ 已完成
- 添加 `getQiniuRegion()` 函数
- 修改 `uploadToQiniu()` 使用动态区域
- 验证 `qiniu-js` 依赖已安装
- 创建测试文档
⏭️ 下一步
- 重启后端服务
- 测试文件上传功能
- 验证区域配置正确
@@ -0,0 +1,248 @@
# 七牛云区域配置流程图
## 问题:区域不匹配
```
┌─────────────────────────────────────────────────────────────┐
│ 修复前(错误) │
└─────────────────────────────────────────────────────────────┘
前端代码 后端数据库 七牛云服务器
┌──────────┐ ┌──────────┐ ┌──────────┐
│ │ │ │ │ │
│ 硬编码: │ │ 配置: │ │ 实际: │
│ region: │ │ qiniu_ │ │ bucket │
│ z0 (华东) │ ────X────> │ region: │ ────X────> │ 在 z2 │
│ │ 不匹配 │ z2 (华南) │ 不匹配 │ (华南) │
│ │ │ │ │ │
└──────────┘ └──────────┘ └──────────┘
┌──────────┐
│ 错误: │
│ incorrect│
│ region │
└──────────┘
```
## 解决方案:动态获取区域
```
┌─────────────────────────────────────────────────────────────┐
│ 修复后(正确) │
└─────────────────────────────────────────────────────────────┘
前端 后端 七牛云
┌──────────┐ ┌──────────┐ ┌──────────┐
│ │ │ │ │ │
│ 1. 请求 │ │ 2. 查询 │ │ │
│ 获取配置 │ ───────> │ 数据库 │ │ │
│ │ │ │ │ │
└──────────┘ └──────────┘ └──────────┘
│ │
│ ▼
│ ┌──────────┐
│ │ 返回: │
│ <─────────── │ region: │
│ │ 'z2' │
│ └──────────┘
┌──────────┐
│ 3. 调用 │
│ getQiniu │
│ Region() │
└──────────┘
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 4. 使用 │ │ │ │ │
│ qiniu. │ │ │ │ 5. 上传 │
│ region. │ ───────────────────────────────> │ 成功 ✓ │
│ z2 │ 上传到 up-z2.qiniup.com │ │
│ │ │ │
└──────────┘ └──────────┘
```
## 详细流程
```
┌─────────────────────────────────────────────────────────────┐
│ 完整上传流程 │
└─────────────────────────────────────────────────────────────┘
用户操作 前端 后端 七牛云
│ │ │ │
│ 1. 点击上传 │ │ │
├──────────────────> │ │ │
│ │ │ │
│ │ 2. GET /platform/ │ │
│ │ storage/config │ │
│ ├─────────────────────>│ │
│ │ │ │
│ │ │ 3. 查询数据库 │
│ │ │ storage_type: │
│ │ │ 'qiniu' │
│ │ │ qiniu_region: │
│ │ │ 'z2' │
│ │ │ │
│ │ 4. 返回配置 │ │
│ │ { storageType: │ │
│ │ 'qiniu', │ │
│ │ qiniuRegion: 'z2' }│ │
│ │<─────────────────────│ │
│ │ │ │
│ │ 5. GET /platform/ │ │
│ │ qiniu/token │ │
│ ├─────────────────────>│ │
│ │ │ │
│ │ │ 6. 生成上传凭证 │
│ │ │ (使用 AccessKey │
│ │ │ 和 SecretKey) │
│ │ │ │
│ │ 7. 返回 token 和配置 │ │
│ │ { token: '...', │ │
│ │ region: 'z2', │ │
│ │ bucket: '...', │ │
│ │ uploadUrl: │ │
│ │ 'up-z2.qiniup.com' } │
│ │<─────────────────────│ │
│ │ │ │
│ │ 8. 调用 │ │
│ │ getQiniuRegion('z2') │
│ │ 返回: qiniu.region.z2 │
│ │ │ │
│ │ 9. 直接上传文件 │ │
│ │ (使用 qiniu-js SDK) │
│ ├──────────────────────────────────────────> │
│ │ │ │
│ │ │ │ 10. 接收文件
│ │ │ │ 存储到 z2
│ │ │ │ (华南)
│ │ │ │
│ │ 11. 返回上传结果 │ │
│ │ { key: '...', │ │
│ │ hash: '...', │ │
│ │ size: ... } │ │
│ │<──────────────────────────────────────────│
│ │ │ │
│ │ 12. POST /platform/ │ │
│ │ qiniu/save │ │
│ │ { key, hash, size, │ │
│ │ name, mimeType } │ │
│ ├─────────────────────>│ │
│ │ │ │
│ │ │ 13. 保存到数据库 │
│ │ │ system_file 表 │
│ │ │ │
│ │ 14. 返回文件信息 │ │
│ │ { url: '...', │ │
│ │ id: 123, │ │
│ │ name: '...' } │ │
│ │<─────────────────────│ │
│ │ │ │
│ 15. 显示上传成功 │ │ │
│<──────────────────│ │ │
│ │ │ │
```
## 区域映射关系
```
┌─────────────────────────────────────────────────────────────┐
│ getQiniuRegion() 函数 │
└─────────────────────────────────────────────────────────────┘
数据库配置 函数输入 函数输出 上传地址
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐
│ qiniu_ │ │ │ │ │ │ │
│ region: │ ───> │ 'z0' │ ───> │ qiniu. │ ───> │ up-z0.qiniup.com │
│ 'z0' │ │ │ │ region.z0│ │ (华东) │
└──────────┘ └──────────┘ └──────────┘ └──────────────────┘
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐
│ qiniu_ │ │ │ │ │ │ │
│ region: │ ───> │ 'z1' │ ───> │ qiniu. │ ───> │ up-z1.qiniup.com │
│ 'z1' │ │ │ │ region.z1│ │ (华北) │
└──────────┘ └──────────┘ └──────────┘ └──────────────────┘
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐
│ qiniu_ │ │ │ │ │ │ │
│ region: │ ───> │ 'z2' │ ───> │ qiniu. │ ───> │ up-z2.qiniup.com │
│ 'z2' │ │ │ │ region.z2│ │ (华南) ✓ 当前 │
└──────────┘ └──────────┘ └──────────┘ └──────────────────┘
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐
│ qiniu_ │ │ │ │ │ │ │
│ region: │ ───> │ 'na0' │ ───> │ qiniu. │ ───> │ up-na0.qiniup.com│
│ 'na0' │ │ │ │ region. │ │ (北美) │
│ │ │ │ │ na0 │ │ │
└──────────┘ └──────────┘ └──────────┘ └──────────────────┘
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐
│ qiniu_ │ │ │ │ │ │ │
│ region: │ ───> │ 'as0' │ ───> │ qiniu. │ ───> │ up-as0.qiniup.com│
│ 'as0' │ │ │ │ region. │ │ (新加坡) │
│ │ │ │ │ as0 │ │ │
└──────────┘ └──────────┘ └──────────┘ └──────────────────┘
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────────┐
│ qiniu_ │ │ │ │ │ │ │
│ region: │ ───> │'cn-east-2'──────>│ qiniu. │ ───> │up-cn-east-2.qiniup. │
│'cn-east-2' │ │ │ region. │ │com (华东-浙江2) │
│ │ │ │ │ cnEast2 │ │ │
└──────────┘ └──────────┘ └──────────┘ └──────────────────────┘
```
## 代码对比
### 修复前(硬编码)
```javascript
// ❌ 错误:硬编码区域
const config = {
useCdnDomain: true,
region: qiniu.region.z0, // 总是使用华东
};
```
### 修复后(动态配置)
```javascript
// ✓ 正确:动态获取区域
const { region } = tokenRes.data; // 从后端获取: 'z2'
const qiniuRegion = getQiniuRegion(region); // 转换为: qiniu.region.z2
const config = {
useCdnDomain: true,
region: qiniuRegion, // 使用正确的区域
};
```
## 优势对比
```
┌─────────────────────────────────────────────────────────────┐
│ 硬编码方式 │
└─────────────────────────────────────────────────────────────┘
缺点:
❌ 区域固定,无法灵活切换
❌ 修改区域需要改代码、重新部署
❌ 容易出现区域不匹配错误
❌ 不同环境需要不同的代码版本
┌─────────────────────────────────────────────────────────────┐
│ 动态配置方式 │
└─────────────────────────────────────────────────────────────┘
优点:
✓ 区域灵活,可随时切换
✓ 修改区域只需改数据库配置
✓ 自动使用正确的区域
✓ 所有环境使用同一套代码
✓ 支持多租户不同区域配置
```
## 更新日期
2026-04-09
+443
View File
@@ -0,0 +1,443 @@
# 七牛云直传配置说明
## 概述
新的上传机制实现了前端直接上传到七牛云,不再通过后端中转,大幅提升大文件上传效率。
## 上传流程对比
### 旧流程(低效)
```
前端 → 后端服务器 → 七牛云
(中转暂存)
```
问题:
- 大文件需要先上传到服务器,再由服务器上传到七牛云
- 占用服务器带宽和磁盘空间
- 上传时间翻倍
- 服务器压力大
### 新流程(高效)
```
前端 → 七牛云(直传)
后端 → 数据库(仅保存记录)
```
优势:
- 前端直接上传到七牛云,不经过服务器
- 节省服务器资源
- 上传速度快
- 支持断点续传
## 安装依赖
### 前端安装七牛云 SDK
```bash
cd platform
npm install qiniu-js
```
或使用 yarn
```bash
yarn add qiniu-js
```
## 后端 API
### 1. 获取存储配置
**接口**: `GET /platform/storage/config`
**响应**:
```json
{
"code": 200,
"data": {
"storageType": "qiniu", // 或 "local"
"qiniuDomain": "http://7colud.yunzer.cn",
"qiniuRegion": "z0"
}
}
```
### 2. 获取上传凭证
**接口**: `GET /platform/qiniu/token`
**响应**:
```json
{
"code": 200,
"data": {
"token": "七牛云上传token",
"domain": "http://7colud.yunzer.cn",
"bucket": "your-bucket",
"region": "z0",
"keyPrefix": "2026/04/09/1775722615052606500",
"expires": 1712654400,
"uploadUrl": "https://up-z0.qiniup.com"
}
}
```
### 3. 保存文件记录
**接口**: `POST /platform/qiniu/save`
**请求**:
```json
{
"key": "2026/04/09/1775722615052606500.png",
"hash": "FhGxwBzoLwO_RGws...",
"size": 1024000,
"name": "screenshot.png",
"mimeType": "image/png",
"cate": 0
}
```
**响应**:
```json
{
"code": 200,
"data": {
"url": "http://7colud.yunzer.cn/2026/04/09/1775722615052606500.png",
"id": 123,
"name": "screenshot.png",
"key": "2026/04/09/1775722615052606500.png"
}
}
```
## 前端使用
### 基础用法
```javascript
import { smartUpload } from '@/utils/qiniuUpload';
// 自动选择上传方式(本地或七牛云)
const result = await smartUpload(file, {
cate: 0, // 文件分类
onProgress: (progress) => {
console.log('上传进度:', progress.percent + '%');
console.log('已上传:', progress.loaded);
console.log('总大小:', progress.total);
},
});
console.log('上传成功:', result);
// { url: '...', id: 123, name: '...', key: '...' }
```
### 在组件中使用
```vue
<template>
<el-upload
:http-request="handleUpload"
:on-progress="handleProgress"
>
<el-button>上传文件</el-button>
</el-upload>
<el-progress v-if="uploading" :percentage="uploadPercent" />
</template>
<script setup>
import { ref } from 'vue';
import { smartUpload } from '@/utils/qiniuUpload';
import { ElMessage } from 'element-plus';
const uploading = ref(false);
const uploadPercent = ref(0);
async function handleUpload(options) {
const file = options.file?.raw || options.file;
uploading.value = true;
try {
const result = await smartUpload(file, {
cate: 0,
onProgress: (progress) => {
uploadPercent.value = progress.percent;
},
});
ElMessage.success('上传成功');
options.onSuccess?.(result);
} catch (error) {
ElMessage.error(error.message || '上传失败');
options.onError?.(error);
} finally {
uploading.value = false;
}
}
</script>
```
### 批量上传
```javascript
import { batchUpload } from '@/utils/qiniuUpload';
const files = [file1, file2, file3];
const results = await batchUpload(files, {
cate: 0,
onFileProgress: (file, progress) => {
console.log(`${file.name}: ${progress.percent}%`);
},
onFileComplete: (file, result) => {
console.log(`${file.name} 上传成功:`, result);
},
onFileError: (file, error) => {
console.error(`${file.name} 上传失败:`, error);
},
});
console.log('所有文件上传完成:', results);
```
## 工作原理
### 1. 智能选择上传方式
`smartUpload` 函数会自动检测后端配置:
```javascript
// 1. 获取存储配置
const config = await getStorageConfig();
// 2. 根据配置选择上传方式
if (config.storageType === 'qiniu') {
// 七牛云直传
return uploadToQiniu(file, options);
} else {
// 本地上传(通过后端)
return uploadToLocal(file, options);
}
```
### 2. 七牛云直传流程
```javascript
// 1. 获取上传凭证
const tokenRes = await getQiniuToken();
const { token, keyPrefix } = tokenRes.data;
// 2. 生成文件 key
const key = `${keyPrefix}.${ext}`;
// 3. 使用七牛云 SDK 直接上传
const observable = qiniu.upload(file, key, token);
// 4. 监听上传进度
observable.subscribe({
next(res) {
// 进度回调
onProgress(res.total.percent);
},
complete(res) {
// 上传完成,保存记录到数据库
await saveFileRecord({
key: res.key,
hash: res.hash,
size: file.size,
name: file.name,
});
},
});
```
### 3. 本地上传流程
```javascript
// 通过后端中转(兼容本地存储)
const formData = new FormData();
formData.append('file', file);
const res = await request({
url: '/platform/uploadfile',
method: 'post',
data: formData,
onUploadProgress: (e) => {
onProgress(e.loaded / e.total * 100);
},
});
```
## 配置说明
### 七牛云区域配置
| 区域代码 | 区域名称 | 上传地址 |
|---------|---------|---------|
| z0 | 华东 | https://up-z0.qiniup.com |
| z1 | 华北 | https://up-z1.qiniup.com |
| z2 | 华南 | https://up-z2.qiniup.com |
| na0 | 北美 | https://up-na0.qiniup.com |
| as0 | 新加坡 | https://up-as0.qiniup.com |
| cn-east-2 | 华东-浙江2 | https://up-cn-east-2.qiniup.com |
### 上传策略配置
后端生成 token 时的策略:
```go
putPolicy := storage.PutPolicy{
Scope: cfg.QiniuBucket,
ReturnBody: `{"key":"$(key)","hash":"$(etag)","size":$(fsize),"mimeType":"$(mimeType)"}`,
Expires: 3600, // 1小时有效期
}
```
## 安全性
### 1. Token 有效期
上传 token 有效期为 1 小时,过期后需要重新获取。
### 2. 权限验证
- 获取 token 需要登录认证
- 保存文件记录需要登录认证
- 文件记录关联到当前用户和租户
### 3. 文件去重
通过 MD5 检查文件是否已存在,避免重复上传。
## 性能优化
### 1. 断点续传
七牛云 SDK 支持断点续传(大文件自动分片):
```javascript
const config = {
useCdnDomain: true,
region: qiniu.region.z0,
chunkSize: 4, // 分片大小(MB
concurrentRequestLimit: 3, // 并发上传数
};
const observable = qiniu.upload(file, key, token, putExtra, config);
```
### 2. CDN 加速
启用 CDN 域名加速上传:
```javascript
const config = {
useCdnDomain: true, // 使用 CDN 加速
};
```
### 3. 并发上传
批量上传时可以控制并发数:
```javascript
// 限制同时上传 3 个文件
const concurrency = 3;
const results = [];
for (let i = 0; i < files.length; i += concurrency) {
const batch = files.slice(i, i + concurrency);
const batchResults = await Promise.all(
batch.map(file => smartUpload(file, options))
);
results.push(...batchResults);
}
```
## 故障排查
### 1. 上传失败:获取 token 失败
**错误**: "当前未配置七牛云存储"
**解决**:
- 检查数据库存储配置
- 确认 `storage_type``'qiniu'`
- 确认七牛云配置完整
### 2. 上传失败:token 无效
**错误**: "401 Unauthorized"
**解决**:
- 检查 AccessKey 和 SecretKey 是否正确
- 检查 token 是否过期(1小时有效期)
- 重新获取 token
### 3. 上传失败:bucket 不存在
**错误**: "no such bucket"
**解决**:
- 检查 bucket 名称是否正确
- 检查 bucket 是否在对应区域
- 登录七牛云控制台确认
### 4. 保存记录失败
**错误**: "保存文件记录失败"
**解决**:
- 检查数据库连接
- 检查文件信息是否完整
- 查看后端日志
## 迁移指南
### 从旧版本迁移
1. **安装依赖**:
```bash
npm install qiniu-js
```
2. **更新导入**:
```javascript
// 旧版本
import { uploadFile } from '@/api/file';
// 新版本
import { smartUpload } from '@/utils/qiniuUpload';
```
3. **更新上传代码**:
```javascript
// 旧版本
const formData = new FormData();
formData.append('file', file);
const res = await uploadFile(formData, { cate: 0 });
// 新版本
const result = await smartUpload(file, { cate: 0 });
```
4. **兼容性**:
- `smartUpload` 会自动检测存储配置
- 如果配置为本地存储,会自动使用旧的上传方式
- 无需修改其他代码
## 相关文件
### 后端
- `go/controllers/qiniu_upload.go` - 七牛云上传控制器
- `go/routers/platform/platform.go` - 路由配置
- `go/services/storage_service.go` - 存储服务
### 前端
- `platform/src/utils/qiniuUpload.js` - 七牛云上传工具
- `platform/src/views/platform/softwareupgrade/components/edit.vue` - 软件升级组件(示例)
## 更新日期
2026-04-09
+101
View File
@@ -0,0 +1,101 @@
# 图片URL处理说明
## 问题描述
在使用七牛云存储时,上传的图片URL会出现重复拼接的问题:
```
错误: http://localhost:8081http://7cloud.yunzer.cn/2026/04/09/xxx.png
正确: http://7cloud.yunzer.cn/2026/04/09/xxx.png
```
## 原因分析
1. **本地存储**返回的URL是相对路径:`/uploads/2026/04/09/xxx.png`
2. **七牛云存储**返回的URL是完整URL:`http://7cloud.yunzer.cn/2026/04/09/xxx.png`
3. 前端的 `getFileUrl` 方法会自动拼接 `VITE_API_BASE_URL`
4. 导致七牛云URL被重复拼接
## 解决方案
### 1. 创建通用工具函数
文件:`platform/src/utils/url.js`
```javascript
export function getFileUrl(url) {
if (!url) return '';
// 如果URL已经是完整的URL,直接返回
if (url.startsWith('http://') || url.startsWith('https://')) {
return url;
}
// 否则拼接API基础URL
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '';
return `${API_BASE_URL}${url}`;
}
```
### 2. 在组件中使用
```vue
<script setup>
import { getFileUrl } from '@/utils/url';
// 使用工具函数
const imageUrl = getFileUrl(file.url);
</script>
```
## 使用示例
### 本地存储
```javascript
const url = '/uploads/2026/04/09/xxx.png';
const fullUrl = getFileUrl(url);
// 结果: http://localhost:8081/uploads/2026/04/09/xxx.png
```
### 七牛云存储
```javascript
const url = 'http://7cloud.yunzer.cn/2026/04/09/xxx.png';
const fullUrl = getFileUrl(url);
// 结果: http://7cloud.yunzer.cn/2026/04/09/xxx.png
```
## 需要修改的文件
所有使用 `getFileUrl``getEnvUrl` 的组件都应该使用统一的工具函数:
-`platform/src/views/system/fileManager/index.vue`
-`platform/src/views/moduleshop/center/index.vue`
-`platform/src/views/apps/babyhealth/babys/index.vue`
-`platform/src/views/apps/babyhealth/babys/components/edit.vue`
- ⏳ 其他使用图片URL的组件
## 最佳实践
1. **统一使用工具函数**:不要在组件中重复定义 `getFileUrl`
2. **判断完整URL**:始终检查URL是否已经是完整URL
3. **兼容两种存储**:确保本地存储和七牛云存储都能正常工作
## 测试清单
- [x] 本地存储图片显示正常
- [x] 七牛云存储图片显示正常
- [x] 图片预览功能正常
- [ ] 视频文件显示正常
- [ ] 文档下载功能正常
## 相关文档
- [存储配置功能](../go/docs/README_STORAGE.md)
- [七牛云配置指南](../go/docs/storage-config-guide.md)
---
**修复时间**: 2024-01-01
**修复人员**: AI Assistant
+11
View File
@@ -0,0 +1,11 @@
//拼接接口路径
const getEnvUrl = (path: string) => {
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
return `${API_BASE_URL}${path}`;
};
用例:
<img :src="getEnvUrl(module.thumb)" :alt="module.title" />
const url = getEnvUrl('/platform/moduleCenter/modules');
+12
View File
@@ -0,0 +1,12 @@
import { onMounted } from "vue";
import { getMenus } from "@/api/menu";
onMounted(async () => {
try{
const response = await getMenus();
} catch (error) {
console.error('获取菜单数据失败:', error);
}
});
+126
View File
@@ -0,0 +1,126 @@
# 文件上传超时配置说明
## 修改内容
移除了文件上传的超时限制,并增加了文件大小限制,允许大文件(如软件安装包)上传完成而不会因超时或大小限制中断。
## 前端修改
### 修改位置
`platform/src/api/file.js` 中的 `uploadFile` 函数
### 修改详情
#### 修改前
```javascript
const config = {
url: "/platform/uploadfile",
method: "post",
data: formData,
timeout: 2 * 60 * 60 * 1000, // 2小时超时
};
```
#### 修改后
```javascript
const config = {
url: "/platform/uploadfile",
method: "post",
data: formData,
timeout: 0, // 不设置超时时间,等待文件上传完毕
};
```
## 后端修改
### 1. 文件大小限制调整
#### 修改位置
`go/controllers/platform_file.go`
#### 修改详情
##### 修改前
```go
const fileUploadMaxMB = 200 // 200MB
const fileUploadMaxBytes = fileUploadMaxMB * 1024 * 1024
```
##### 修改后
```go
const fileUploadMaxMB = 2048 // 2GB,适用于大型软件安装包
const fileUploadMaxBytes = fileUploadMaxMB * 1024 * 1024
```
### 2. 服务器超时配置
#### 修改位置
`go/conf/app.conf`
#### 新增配置
```ini
# 服务器超时配置(支持大文件上传)
# 0 表示不设置超时限制
ServerTimeOut = 0
# 最大请求体大小(字节),0 表示不限制
MaxMemory = 0
```
## 影响范围
此修改影响所有使用 `uploadFile` 函数的文件上传功能,包括但不限于:
- 软件升级安装包上传 (`platform/src/views/platform/softwareupgrade/components/edit.vue`)
- 文件管理器上传 (`platform/src/views/system/fileManager/index.vue`)
- 其他调用 `uploadFile` API 的组件
## 技术说明
### 前端
- `timeout: 0` 在 axios 中表示不设置超时限制
- 上传进度仍然会正常显示(通过 `onUploadProgress` 回调)
- 用户可以随时取消上传操作
### 后端
- 文件大小限制从 200MB 提升到 2GB
- `ServerTimeOut = 0` 表示服务器不设置请求超时
- `MaxMemory = 0` 表示不限制请求体大小
- 适用于大文件(如软件安装包)的上传场景
## CORS 问题说明
如果遇到 CORS 错误,请确认:
1. 后端 `go/routers/router.go` 中已配置 CORS 中间件
2. 允许的请求头包含 `Authorization`
3. 生产环境中,需要将 `Access-Control-Allow-Origin` 设置为具体的前端域名
当前配置:
```go
ctx.Output.Header("Access-Control-Allow-Origin", "*")
ctx.Output.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS")
ctx.Output.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
```
## 注意事项
1. 修改后需要重启 Go 后端服务才能生效
2. 对于超大文件(>2GB),建议考虑使用分片上传或断点续传机制
3. 生产环境建议配置 Nginx 等反向代理的超时和大小限制
4. 监控服务器磁盘空间,确保有足够空间存储大文件
## 相关文件
### 前端
- `platform/src/api/file.js` - 文件上传 API
- `platform/src/utils/request.js` - axios 请求配置
- `platform/src/views/platform/softwareupgrade/components/edit.vue` - 软件升级上传组件
### 后端
- `go/controllers/platform_file.go` - 文件上传控制器
- `go/conf/app.conf` - 服务器配置
- `go/routers/router.go` - CORS 配置
## 修改日期
2026-04-09
+23
View File
@@ -0,0 +1,23 @@
import { useAuthStore } from '@/stores/auth';
import { onMounted } from 'vue';
// 使用 auth store 获取用户信息
const authStore = useAuthStore();
// 获取租户ID
const tenantId = (authStore.user as any)?.tid;
// 获取用户信息
const userInfo = authStore.user;
if (userInfo && userInfo.id) {
console.log('用户名:', userInfo.username || userInfo.nickname);
console.log('用户ID:', userInfo.id);
console.log('角色:', userInfo.role);
} else {
console.log('未找到用户信息或用户未登录');
}
onMounted(() => {
getUserInfo();
});
+90
View File
@@ -0,0 +1,90 @@
<el-form-item label="默认图片" prop="image">
<div class="uploads">
<el-upload v-model:file-list="fileList" :action="uploadUrl" list-type="picture-card"
:headers="uploadHeaders" :limit="1" :auto-upload="false" :before-upload="beforeImgUpload"
:on-success="handleImgSuccess">
<el-icon>
<Plus />
</el-icon>
<template #file="{ file }">
<div>
<img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
<span class="el-upload-list__item-actions">
<span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
<el-icon>
<ZoomIn />
</el-icon>
</span>
<span class="el-upload-list__item-delete" @click="handleRemove(file)">
<el-icon>
<Delete />
</el-icon>
</span>
</span>
</div>
</template>
</el-upload>
<el-dialog v-model="dialogVisible">
<img w-full :src="dialogImageUrl" alt="Preview Image" />
</el-dialog>
<div class="upload-tip">
<span>建议尺寸:250px × 140px</span>
</div>
</div>
</el-form-item>
import { uploadFile } from '@/api/file.js';
import { ElMessage, ElUpload } from 'element-plus'
// 上传相关
const fileList = ref<any[]>([])
const dialogVisible = ref(false)
const dialogImageUrl = ref('')
function beforeImgUpload(file: File) {
const isImage = file.type.startsWith('image/')
const isLt10M = file.size / 1024 / 1024 < 10
if (!isImage) ElMessage.error('仅支持图片格式')
if (!isLt10M) ElMessage.error('图片大小不能超过10MB')
return isImage && isLt10M
}
function handleImgUpload(file: File) {
const formData = new FormData()
formData.append('file', file)
formData.append('cate', 'article')
uploadFile(formData).then((res: any) => {
if (res?.url) {
formData.image = res.url
fileList.value = [{
name: file.name,
url: res.url
}]
}
}).catch((error: any) => {
ElMessage.error('上传失败:' + (error.msg || '未知错误'))
})
}
function handlePictureCardPreview(file: any) {
dialogImageUrl.value = file.url
dialogVisible.value = true
}
function handleRemove(file: any) {
fileList.value = []
formData.image = ''
}
.uploads{
display: flex;
flex-direction: column;
}
.upload-tip {
font-size: 12px;
color: #999;
}
+27
View File
@@ -0,0 +1,27 @@
````
<template>
{{ (xxxxxxxxDict.find(item => item.dict_value == String(model?.status)) || {}).dict_label || '-' }}
</template>
<script lang="ts" setup>
import { onMounted } from 'vue';
import { useDictStore } from '@/stores/dict';
// 字典store
const dictStore = useDictStore();
const xxxxxxxxDict = ref<any[]>([]);
// 获取审核状态字典
const fetchxxxxxxxxDict = async () => {
try {
xxxxxxxxDict.value = await dictStore.getDictItems('article_status');
console.log(xxxxxxxxDict.value);
} catch (err) {
console.error('获取文章状态字典失败:', err);
}
};
onMounted(() => {
fetchxxxxxxxxDict();
});
</script>
````
+16
View File
@@ -0,0 +1,16 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/assets/svgs/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>后台管理系统</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
+4964
View File
File diff suppressed because it is too large Load Diff
+40
View File
@@ -0,0 +1,40 @@
{
"name": "pc",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --open",
"clean": "node scripts/clean-dist.mjs",
"build": "node --max-old-space-size=4096 ./node_modules/vite/bin/vite.js build",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@wangeditor/editor": "^5.1.23",
"axios": "^1.13.1",
"chart": "^0.1.2",
"chart.js": "^4.5.1",
"docx-preview": "^0.3.7",
"echarts": "^6.0.0",
"element-plus": "^2.11.7",
"less": "^4.4.2",
"marked": "^16.4.1",
"os": "^0.1.2",
"pinia": "^3.0.3",
"qiniu-js": "^3.4.4",
"vue": "^3.5.22",
"vue-img-cutter": "^3.0.7",
"vue-router": "^4.6.3",
"vue3-pdf-app": "^1.0.3",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@types/node": "^24.10.7",
"@vitejs/plugin-vue": "^6.0.1",
"typescript": "^5.9.3",
"unplugin-auto-import": "^20.2.0",
"unplugin-vue-components": "^30.0.0",
"vite": "^7.1.7"
}
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+49
View File
@@ -0,0 +1,49 @@
/**
* 构建前删除 dist带重试缓解 Windows Vite emptyDir EPERM文件被资源管理器预览杀毒vite preview 等占用
*/
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(__dirname, "..");
const dirs = [path.join(root, "dist"), path.join(root, "output")];
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function rmDirWithRetry(dir) {
for (let i = 0; i < 10; i++) {
if (!fs.existsSync(dir)) {
return true;
}
try {
fs.rmSync(dir, { recursive: true, force: true });
return true;
} catch (e) {
const code = e && typeof e === "object" && "code" in e ? e.code : "";
const retryable = code === "EPERM" || code === "EBUSY" || code === "ENOTEMPTY";
if (!retryable || i === 9) {
console.error(`[clean-dist] 无法删除 ${path.relative(root, dir) || dir}:`, e instanceof Error ? e.message : e);
return false;
}
await sleep(350 * (i + 1));
}
}
return false;
}
async function main() {
let ok = true;
for (const dir of dirs) {
const r = await rmDirWithRetry(dir);
if (!r) ok = false;
}
if (!ok) {
console.error(
"请关闭占用上述目录的程序:资源管理器预览、vite preview、IDE、杀毒实时扫描等后执行 npm run clean。"
);
process.exit(1);
}
}
await main();
+14
View File
@@ -0,0 +1,14 @@
<script setup>
</script>
<template>
<router-view />
</template>
<style scoped>
#app {
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
+93
View File
@@ -0,0 +1,93 @@
import request from '@/utils/request';
function base(module) {
return `/platform/accountPool/${module}`;
}
export function getAccountPoolList(module, params) {
return request({
url: `${base(module)}/list`,
method: 'get',
params,
});
}
export function addAccountPool(module, data) {
return request({
url: `${base(module)}/add`,
method: 'post',
data,
});
}
export function batchAddAccountPool(module, rows) {
return request({
url: `${base(module)}/batchAdd`,
method: 'post',
data: { rows },
});
}
export function getAccountPoolDetail(module, id) {
return request({
url: `${base(module)}/detail/${id}`,
method: 'get',
});
}
export function extractAccountPool(module, data) {
return request({
url: `${base(module)}/extract`,
method: 'post',
data,
});
}
export function updateAccountPoolRemark(module, data) {
return request({
url: `${base(module)}/updateRemark`,
method: 'post',
data,
});
}
export function setAccountPoolUnavailable(module, data) {
return request({
url: `${base(module)}/setUnavailable`,
method: 'post',
data,
});
}
export function updateAccountPoolPlatform(module, data) {
return request({
url: `${base(module)}/updatePlatform`,
method: 'post',
data,
});
}
export function unextractAccountPool(module, data) {
return request({
url: `${base(module)}/unextract`,
method: 'post',
data,
});
}
export function replenishAccountPool(module, data) {
return request({
url: `${base(module)}/replenish`,
method: 'post',
data,
});
}
/** 使用厂商 Token 探测是否可用(服务端转发)。Cursor 传 { id, accessToken }(会话 JWT)以便回写 is_used;仅传 accessToken 也可探测但不更新库;Windsurf/Kiro 传 { id } */
export function probeAccountPoolToken(module, data) {
return request({
url: `${base(module)}/probeToken`,
method: 'post',
data,
});
}
+18
View File
@@ -0,0 +1,18 @@
// 数据统计相关API
import request from "@/utils/request";
// 获取内容统计
export function getContentStats() {
return request({
url: "/platform/contentstats",
method: "get",
});
}
// 获取用户统计
export function getUserStats() {
return request({
url: "/platform/usersstats",
method: "get",
});
}
+170
View File
@@ -0,0 +1,170 @@
// 文章管理相关API
import request from "@/utils/request";
// 获取文章列表
export function listArticles(params) {
return request({
url: `/platform/articlesList`,
method: "get",
params,
});
}
// 获取文章所有文章
export function listAllArticles(params) {
return request({
url: `/platform/allarticles`,
method: "get",
params,
});
}
// 获取文章详情
export function getArticle(id) {
return request({
url: `/platform/articles/${id}`,
method: "get",
});
}
// 创建文章
export function createArticle(data) {
return request({
url: '/platform/createarticle',
method: 'post',
data,
});
}
// 编辑文章
export function editArticle(id, data) {
return request({
url: `/platform/editarticle/${id}`,
method: 'post',
data,
});
}
// 删除文章
export function deleteArticle(id) {
return request({
url: `/platform/deletearticle/${id}`,
method: "delete",
});
}
// 发布文章
export function publishArticle(id,uid) {
return request({
url: `/platform/publisharticle/${id}`,
method: 'post',
data: {
uid
}
});
}
// 下架文章
export function unPublishArticle(id) {
return request({
url: `/platform/unPublisharticle/${id}`,
method: 'post'
});
}
// 文章推荐
export function articleRecommend(id) {
return request({
url: `/platform/articleRecommend/${id}`,
method: 'post'
});
}
// 取消文章推荐
export function unArticleRecommend(id) {
return request({
url: `/platform/unArticleRecommend/${id}`,
method: 'post'
});
}
// 文章置顶
export function articleTop(id) {
return request({
url: `/platform/articleTop/${id}`,
method: 'post'
});
}
// 取消文章置顶
export function unArticleTop(id) {
return request({
url: `/platform/unArticleTop/${id}`,
method: 'post'
});
}
////////////////////////////分类相关////////////////////////////
// 获取所有分类列表
export function allCategories(params) {
return request({
url: `/platform/allcategories`,
method: "get",
params,
});
}
// 获取分类列表
export function listCategories(params) {
return request({
url: `/platform/categories`,
method: "get",
params,
});
}
// 获取分类详情
export function getCategory(id) {
return request({
url: `/platform/categories/${id}`,
method: "get",
});
}
// 创建分类
export function createCategory(data) {
return request({
url: `/platform/createCategory`,
method: "post",
data,
});
}
// 更新分类
export function editCategory(id, data) {
return request({
url: `/platform/editCategory/${id}`,
method: "post",
data,
});
}
// 删除分类
export function deleteCategory(id) {
return request({
url: `/platform/categories/${id}`,
method: "delete",
});
}
// 更新分类状态
export function updateCategoryStatus(id, status) {
return request({
url: `/platform/categories/${id}/status`,
method: "patch",
data: { status },
});
}
+179
View File
@@ -0,0 +1,179 @@
import request from "@/utils/request";
/*************************************************
****************** 宝贝相关接口 ******************
*************************************************/
/**
* 获取宝贝列表
* @returns {Promise}
*/
export function getBabyList() {
return request({
url: '/platform/babys/list',
method: 'get'
});
}
/**
* 获取宝贝详情
* @param {number} id 宝贝ID
* @returns {Promise}
*/
export function getBabyDetail(id) {
return request({
url: `/platform/babys/${id}`,
method: "get",
});
}
/**
* 创建宝贝数据
* @param {Object} data 宝贝数据
* @returns {Promise}
*/
export function createBaby(data) {
return request({
url: "/platform/babys",
method: "post",
data: data,
headers: {
"Content-Type": "multipart/form-data"
}
});
}
// 更新宝贝信息
export function editBaby(id, data) {
return request({
url: `/platform/baby/update/${id}`,
method: 'post',
data: data
});
}
/**
* 删除宝贝数据
* @param {number} id 宝贝ID
* @returns {Promise}
*/
export function deleteBaby(id) {
return request({
url: `/platform/babys/${id}`,
method: "delete",
});
}
/**
* 绑定父母
* @param {number} id 宝贝ID
* @param {Object} data 绑定数据
* @returns {Promise}
*/
export function bindParent(id, data) {
return request({
url: `/platform/babys/bindparents/${id}`,
method: "post",
data: data,
});
}
/**
* 获取父母
* @param {number} id 宝贝ID
* @returns {Promise}
*/
export function getParents(id) {
return request({
url: `/platform/babys/getParents/${id}`,
method: "get",
});
}
/*************************************************
****************** 用户相关接口 ******************
*************************************************/
/**
* 获取用户列表
* @returns {Promise}
*/
export function getUserList() {
return request({
url: "/platform/babyhealthUser/list",
method: "get",
});
}
/**
* 获取用户详情
* @param {number} id 用户ID
* @returns {Promise}
*/
export function getUserDetail(id) {
return request({
url: `/platform/babyhealthUser/detail/${id}`,
method: "get",
});
}
/**
* 创建用户数据
* @param {Object} data 用户数据
* @returns {Promise}
*/
export function createUser(data) {
return request({
url: "/platform/babyhealthUser/create",
method: "post",
data: data,
headers: {
"Content-Type": "multipart/form-data"
}
});
}
/**
* 更新用户数据
* @param {number} id 用户ID
* @param {Object} data 更新的数据
* @returns {Promise}
*/
export function updateUser(id, data) {
return request({
url: `/platform/babyhealthUser/update/${id}`,
method: "post",
data: data,
headers: {
"Content-Type": "multipart/form-data"
}
});
}
/**
* 删除用户数据
* @param {number} id 用户ID
* @returns {Promise}
*/
export function deleteUser(id) {
return request({
url: `/platform/babyhealthUser/delete/${id}`,
method: "delete",
});
}
/*************************************************
****************** 仪表盘相关接口 ******************
*************************************************/
/**
* dashborad总体输出
* @returns {Promise}
*/
export function getDashborad() {
return request({
url: "/platform/babyhealthDashborad/dashborad",
method: "get",
});
}
+55
View File
@@ -0,0 +1,55 @@
import request from "@/utils/request";
/**
* 获取所有Banner
* @returns {Promise}
*/
export function getBanners() {
return request({
url: "/platform/allbanners",
method: "get",
});
}
/**
* 创建Banner
* @param {Object} bannerData Banner数据
* @returns {Promise}
*/
export function createBanner(formData) {
return request({
url: "/platform/createbanner",
method: "post",
data: formData,
headers: {
"Content-Type": "multipart/form-data"
}
});
}
/**
* 编辑Banner
* @param {number|string} id Banner ID
* @param {Object} bannerData 更新的数据
* @returns {Promise}
*/
export function editBanner(id, bannerData) {
return request({
url: `/platform/editbanner/${id}`,
method: "post",
data: bannerData,
});
}
/**
* 删除Banner
* @param {number|string} id Banner ID
* @returns {Promise}
*/
export function deleteBanner(id) {
return request({
url: `/platform/deletebanner/${id}`,
method: "delete",
});
}
+78
View File
@@ -0,0 +1,78 @@
import request from "@/utils/request";
/** 投诉建议列表 */
export function getComplaintList(params) {
return request({
url: "/platform/complaint/list",
method: "get",
params,
});
}
export function getComplaintDetail(id) {
return request({
url: `/platform/complaint/${id}`,
method: "get",
});
}
export function createComplaint(data) {
return request({
url: "/platform/complaint",
method: "post",
data,
});
}
export function updateComplaint(id, data) {
return request({
url: `/platform/complaint/${id}`,
method: "post",
data,
});
}
export function deleteComplaint(id) {
return request({
url: `/platform/complaint/${id}`,
method: "delete",
});
}
/** 产品分类(投诉建议用) */
export function getComplaintCategoryList() {
return request({
url: "/platform/complaintCategory/list",
method: "get",
});
}
export function getComplaintCategorySelect() {
return request({
url: "/platform/complaintCategory/select",
method: "get",
});
}
export function createComplaintCategory(data) {
return request({
url: "/platform/complaintCategory",
method: "post",
data,
});
}
export function updateComplaintCategory(id, data) {
return request({
url: `/platform/complaintCategory/${id}`,
method: "post",
data,
});
}
export function deleteComplaintCategory(id) {
return request({
url: `/platform/complaintCategory/${id}`,
method: "delete",
});
}
+33
View File
@@ -0,0 +1,33 @@
import request from '@/utils/request'
export function listContacts(params) {
return request({
url: '/platform/crm/contact/list',
method: 'get',
params,
})
}
export function createContact(data) {
return request({
url: '/platform/crm/contact/add',
method: 'post',
data,
})
}
export function updateContact(data) {
return request({
url: '/platform/crm/contact/edit',
method: 'post',
data,
})
}
export function deleteContact(data) {
return request({
url: '/platform/crm/contact/delete',
method: 'post',
data,
})
}
+65
View File
@@ -0,0 +1,65 @@
import request from '@/utils/request';
const baseUrl = '/platform/cursor/equipment';
export function getCursorEquipmentList(params) {
return request({
url: `${baseUrl}/list`,
method: 'get',
params,
});
}
export function getCursorEquipmentDetail(id) {
return request({
url: `${baseUrl}/detail/${id}`,
method: 'get',
});
}
export function addCursorEquipment(data) {
return request({
url: `${baseUrl}/add`,
method: 'post',
data,
});
}
export function updateCursorEquipment(data) {
return request({
url: `${baseUrl}/update`,
method: 'post',
data,
});
}
export function deleteCursorEquipment(id) {
return request({
url: `${baseUrl}/delete/${id}`,
method: 'post',
});
}
export function activateCursorEquipment(data) {
return request({
url: `${baseUrl}/activate`,
method: 'post',
data,
});
}
export function getCursorEquipmentActivationRecords(params) {
return request({
url: `${baseUrl}/activationRecords`,
method: 'get',
params,
});
}
export function getCursorEquipmentExtractRecords(params) {
return request({
url: `${baseUrl}/extractRecords`,
method: 'get',
params,
});
}
+41
View File
@@ -0,0 +1,41 @@
import request from "@/utils/request";
/**
* 获取平台统计数据平台用户使用
* @returns {Promise}
*/
export function getPlatformStats() {
return request({
url: "/platform/dashboard/platform-stats",
method: "get",
});
}
/**
* 获取租户统计数据租户员工使用
* @returns {Promise}
*/
export function getTenantStats() {
return request({
url: "/platform/dashboard/tenant-stats",
method: "get",
});
}
/**
* 获取用户活动日志操作日志和登录日志
* @param {number} pageNum - 页码
* @param {number} pageSize - 每页数量
* @returns {Promise}
*/
export function getActivityLogs(pageNum = 1, pageSize = 10) {
return request({
url: "/platform/dashboard/user-activity-logs",
method: "get",
params: {
page_num: pageNum,
page_size: pageSize,
},
});
}
+51
View File
@@ -0,0 +1,51 @@
import request from "@/utils/request";
/**
* 获取需求列表
* @returns {Promise}
*/
export function getDemandList() {
return request({
url: "/platform/demandList",
method: "get",
});
}
/**
* 新增需求
* @param {Object} data 需求数据
* @returns {Promise}
*/
export function addDemand(data) {
return request({
url: "/platform/addDemand",
method: "post",
data,
});
}
/**
* 编辑需求
* @param {number} id 需求ID
* @param {Object} data 需求数据
* @returns {Promise}
*/
export function editDemand(id, data) {
return request({
url: `/platform/editDemand/${id}`,
method: "post",
data,
});
}
/**
* 删除需求
* @param {number} id 需求ID
* @returns {Promise}
*/
export function deleteDemand(id) {
return request({
url: `/platform/deleteDemand/${id}`,
method: "post",
});
}
+44
View File
@@ -0,0 +1,44 @@
import request from '@/utils/request';
// 获取租户下的所有部门
export function getTenantDepartments(tenantId) {
return request({
url: `/platform/departments/tenant/${tenantId}`,
method: 'get',
});
}
// 获取部门详情
export function getDepartmentInfo(departmentId) {
return request({
url: `/platform/departments/${departmentId}`,
method: 'get',
});
}
// 添加部门
export function addDepartment(data) {
return request({
url: '/platform/departments',
method: 'post',
data,
});
}
// 更新部门信息
export function editDepartment(departmentId, data) {
return request({
url: `/platform/departments/${departmentId}`,
method: 'put',
data,
});
}
// 删除部门
export function deleteDepartment(departmentId) {
return request({
url: `/platform/departments/${departmentId}`,
method: 'delete',
});
}
+114
View File
@@ -0,0 +1,114 @@
import request from '@/utils/request'
// 获取字典类型列表
export function getDictTypes(params) {
return request({
url: '/platform/dict/types',
method: 'get',
params
})
}
// 根据ID获取字典类型
export function getDictTypeById(id) {
return request({
url: `/platform/dict/types/${id}`,
method: 'get'
})
}
// 添加字典类型
export function addDictType(data) {
return request({
url: '/platform/dict/types',
method: 'post',
data: {
...data,
is_global: data.is_global !== undefined ? data.is_global : 0
}
})
}
// 更新字典类型
export function updateDictType(id, data) {
return request({
url: `/platform/dict/types/${id}`,
method: 'put',
data: {
...data,
is_global: data.is_global !== undefined ? data.is_global : 0
}
})
}
// 删除字典类型
export function deleteDictType(id) {
return request({
url: `/platform/dict/types/${id}`,
method: 'delete'
})
}
// 获取字典项列表
export function getDictItems(params) {
return request({
url: '/platform/dict/items',
method: 'get',
params
})
}
// 根据ID获取字典项
export function getDictItemById(id) {
return request({
url: `/platform/dict/items/${id}`,
method: 'get'
})
}
// 添加字典项
export function addDictItem(data) {
return request({
url: '/platform/dict/items',
method: 'post',
data
})
}
// 更新字典项
export function updateDictItem(id, data) {
return request({
url: `/platform/dict/items/${id}`,
method: 'put',
data
})
}
// 删除字典项
export function deleteDictItem(id) {
return request({
url: `/platform/dict/items/${id}`,
method: 'delete'
})
}
// 根据字典编码获取字典项(用于业务查询)
export function getDictItemsByCode(code, includeDisabled = false) {
return request({
url: `/platform/dict/items/code/${code}`,
method: 'get',
params: {
include_disabled: includeDisabled ? '1' : '0'
}
})
}
// 批量更新字典项排序
export function batchUpdateDictItemSort(data) {
return request({
url: '/platform/dict/items/sort',
method: 'put',
data
})
}
+110
View File
@@ -0,0 +1,110 @@
import request from '@/utils/request'
// ==================== 主域名池管理 ====================
// 获取域名池列表
export function getDomainPoolList(params) {
return request({
url: '/platform/domain/pool/index',
method: 'get',
params
})
}
// 获取启用的主域名列表
export function getEnabledDomains() {
return request({
url: '/platform/domain/pool/getEnabledDomains',
method: 'get'
})
}
// 创建主域名
export function createDomainPool(data) {
return request({
url: '/platform/domain/pool/create',
method: 'post',
data
})
}
// 更新主域名
export function updateDomainPool(data) {
return request({
url: '/platform/domain/pool/update',
method: 'post',
data
})
}
// 删除主域名
export function deleteDomainPool(id) {
return request({
url: `/platform/domain/pool/delete/${id}`,
method: 'delete'
})
}
// 切换主域名状态
export function toggleDomainPoolStatus(id) {
return request({
url: '/platform/domain/pool/toggleStatus',
method: 'post',
data: { id }
})
}
// ==================== 租户域名管理 ====================
// 获取租户域名列表(管理员)
export function getTenantDomainList(params) {
return request({
url: '/platform/domain/tenant/index',
method: 'get',
params
})
}
// 获取当前租户的域名列表
export function getMyDomains(params) {
return request({
url: '/platform/domain/tenant/myDomains',
method: 'get',
params
})
}
// 申请二级域名
export function applyTenantDomain(data) {
return request({
url: '/platform/domain/tenant/apply',
method: 'post',
data
})
}
// 审核租户域名
export function auditTenantDomain(data) {
return request({
url: '/platform/domain/tenant/audit',
method: 'post',
data
})
}
// 禁用/启用租户域名
export function toggleTenantDomainStatus(id) {
return request({
url: '/platform/domain/tenant/toggleStatus',
method: 'post',
data: { id }
})
}
// 删除租户域名
export function deleteTenantDomain(id) {
return request({
url: `/platform/domain/tenant/delete/${id}`,
method: 'delete'
})
}
+36
View File
@@ -0,0 +1,36 @@
import request from "@/utils/request";
/**
* 获取邮箱信息
* @returns {Promise}
*/
export function getEmailInfo() {
return request({
url: "/platform/email/info",
method: "get",
});
}
/**
* 编辑邮箱信息
* @returns {Promise}
*/
export function editEmailInfo(data) {
return request({
url: "/platform/email/editinfo",
method: "post",
data,
});
}
/**
* 发送测试邮件
* @returns {Promise}
*/
export function sendTestEmail(data) {
return request({
url: "/platform/email/sendtestemail",
method: "post",
data,
});
}
+155
View File
@@ -0,0 +1,155 @@
import request from "@/utils/request";
/*************************************************
****************** 组织机构相关接口 ******************
*************************************************/
/**
* 获取组织机构列表
* @returns {Promise}
*/
export function getOrganizationList() {
return request({
url: '/platform/erp/getOrganization',
method: 'get'
});
}
/**
* 获取组织机构详情
* @param {number} id 组织机构ID
* @returns {Promise}
*/
export function getOrganizationDetail(id) {
return request({
url: `/platform/erp/getOrganizationDetail/${id}`,
method: "get",
});
}
/**
* 创建组织机构数据
* @param {Object} data 组织机构数据
* @returns {Promise}
*/
export function createOrganization(data) {
return request({
url: "/platform/erp/createOrganization",
method: "post",
data: data,
headers: {
"Content-Type": "multipart/form-data"
}
});
}
// 更新组织机构信息
export function editOrganization(id, data) {
return request({
url: `/platform/erp/editOrganization/${id}`,
method: 'post',
data: data
});
}
/**
* 删除组织机构数据
* @param {number} id 组织机构ID
* @returns {Promise}
*/
export function deleteOrganization(id) {
return request({
url: `/platform/erp/deleteOrganization/${id}`,
method: "delete",
});
}
/**
* 获取企业单位列表
* @returns {Promise}
*/
export function getCompanys() {
return request({
url: '/platform/erp/getCompanys',
method: 'get'
});
}
/**
* 获取部门列表
* @param {number} parentId 隶属单位ID
* @returns {Promise}
*/
export function getDepartments(parentId) {
return request({
url: '/platform/erp/getDepartments',
method: 'get',
params: parentId ? { parent_id: parentId } : {}
});
}
/*************************************************
****************** 员工相关接口 ******************
*************************************************/
/**
* 获取员工列表
* @param {number} tenantId 租户ID
* @returns {Promise}
*/
export function getEmployeeList(tenantId) {
return request({
url: '/platform/erp/getEmployee',
method: 'get',
params: { tid: tenantId }
});
}
/**
* 获取员工详情
* @param {number} id 员工ID
* @returns {Promise}
*/
export function getEmployeeDetail(id) {
return request({
url: `/platform/erp/getEmployeeDetail/${id}`,
method: "get",
});
}
/**
* 创建员工数据
* @param {Object} data 员工数据
* @returns {Promise}
*/
export function createEmployee(data) {
return request({
url: "/platform/erp/createEmployee",
method: "post",
data: data,
headers: {
"Content-Type": "multipart/form-data"
}
});
}
// 更新员工信息
export function editEmployee(id, data) {
return request({
url: `/platform/erp/editEmployee/${id}`,
method: 'post',
data: data
});
}
/**
* 删除员工数据
* @param {number} id 员工ID
* @returns {Promise}
*/
export function deleteEmployee(id) {
return request({
url: `/platform/erp/deleteEmployee/${id}`,
method: "delete",
});
}
+222
View File
@@ -0,0 +1,222 @@
import request from "@/utils/request";
/**
* 获取用户分类
* @returns {Promise}
*/
export function getUserCate() {
return request({
url: `/platform/usercate`,
method: "get",
});
}
/**
* 获取所有文件支持分页分类关键词与后端 query 一致
* @param {Object} [params] page, pageSize, cate, keyword
* @returns {Promise}
*/
export function getAllFiles(params = {}) {
return request({
url: "/platform/allfiles",
method: "get",
params,
});
}
/**
* 新建文件分组
* @param {Object} data 文件分组数据
* @returns {Promise}
*/
export function createFileCate(data) {
return request({
url: "/platform/createfilecate",
method: "post",
data,
});
}
/**
* 重命名文件分组
* @param {number|string} id 文件分组ID
* @param {Object} data 文件分组数据
* @returns {Promise}
*/
export function renameFileCate(id, data) {
return request({
url: `/platform/renamefilecate/${id}`,
method: "post",
data,
});
}
/**
* 删除文件分组
* @param {number|string} id 文件分组ID
* @returns {Promise}
*/
export function deleteFileCate(id) {
return request({
url: `/platform/deletefilecate/${id}`,
method: "delete",
});
}
/**
* 根据分类ID获取文件
* @param {number|string} id 分类ID
* @param {number} page 页码默认1
* @param {number} pageSize 每页数量默认24
* @param {string} keyword 搜索关键词可选
* @returns {Promise}
*/
export function getCateFiles(id, page = 1, pageSize = 24, keyword = "") {
const params = {
page,
pageSize,
};
if (keyword) {
params.keyword = keyword;
}
return request({
url: `/platform/catefiles/${id}`,
method: "get",
params,
});
}
/**
* 根据文件 ID 获取单条文件信息非分类列表
* @param {number|string} id 文件主键 ID
* @returns {Promise}
*/
export function getFileById(id) {
return request({
url: `/platform/file/${id}`,
method: "get",
});
}
/**
* 上传文件
* @param {FormData} formData 文件数据
* @param {Object} options 额外选项
* @param {string|number} [options.cate] 文件分组0 为未分类
* @param {string|number} [options.tuid] 租户用户 yz_tenant_user.id租户侧上传时传平台管理员不传
* @param {(e: { loaded: number; total?: number }) => void} [options.onUploadProgress] 上传进度浏览器 XHR
* @returns {Promise}
*/
export function uploadFile(formData, options = {}) {
// 0 表示「未分类」,不能用 truthy 判断
if (options.cate !== undefined && options.cate !== null && options.cate !== "") {
formData.append("cate", String(options.cate));
}
if (options.tuid !== undefined && options.tuid !== null && options.tuid !== "") {
formData.append("tuid", String(options.tuid));
}
const config = {
url: "/platform/uploadfile",
method: "post",
data: formData,
// 不设置超时时间,等待文件上传完毕;勿设置 Content-Type(由浏览器自动带 boundary
timeout: 0,
};
if (typeof options.onUploadProgress === "function") {
config.onUploadProgress = options.onUploadProgress;
}
return request(config);
}
/**
* 更新文件信息
* @param {number|string} id 文件ID
* @param {Object} fileData 更新的数据
* @returns {Promise}
*/
export function updateFile(id, fileData) {
return request({
url: `/platform/updatefile/${id}`,
method: "post",
data: fileData,
});
}
/**
* 删除文件
* @param {number|string} id 文件ID
* @returns {Promise}
*/
export function deleteFile(id) {
return request({
url: `/platform/deletefile/${id}`,
method: "delete",
});
}
/**
* 删除文件
* @param {number|string} id 文件ID
* @returns {Promise}
*/
export function deleteFilePermanently(id) {
return request({
url: `/platform/deletefilepermanently/${id}`,
method: "delete",
});
}
/**
* 移动文件
* @param {number|string} id 文件ID
* @param {Object} fileData 更新的数据
* @returns {Promise}
*/
export function moveFile(id, cate) {
return request({
url: `/platform/movefile/${id}`,
method: "get",
params: { cate },
});
}
/**
* 批量删除文件
* @param {Array} ids 文件ID数组
* @returns {Promise}
*/
export function batchDeleteFiles(ids) {
return request({
url: "/platform/batchdeletefiles",
method: "post",
data: { ids },
});
}
/**
* 批量彻底删除文件
* @param {Array} ids 文件ID数组
* @returns {Promise}
*/
export function batchDeleteFilesPermanently(ids) {
return request({
url: "/platform/batchDeleteFilesPermanently",
method: "post",
data: { ids },
});
}
/**
* 批量移动文件
* @param {Array} ids 文件ID数组
* @param {number} cate 目标分类ID
* @returns {Promise}
*/
export function batchMoveFiles(ids, cate) {
return request({
url: "/platform/batchMoveFiles",
method: "post",
data: { ids, cate },
});
}
+77
View File
@@ -0,0 +1,77 @@
import request from '@/utils/request'
/**
* 获取友情链接列表
* @param {Object} params - 查询参数
* @returns {Promise}
*/
export function getFriendlinkList(params) {
return request({
url: '/platform/friendlinks',
method: 'get',
params
})
}
/**
* 获取所有友情链接下拉选择用
* @returns {Promise}
*/
export function getAllFriendlinks() {
return request({
url: '/platform/friendlinks/all',
method: 'get'
})
}
/**
* 添加友情链接
* @param {Object} data - 链接数据
* @returns {Promise}
*/
export function addFriendlink(data) {
return request({
url: '/platform/friendlinks',
method: 'post',
data
})
}
/**
* 更新友情链接
* @param {number} id - 链接ID
* @param {Object} data - 链接数据
* @returns {Promise}
*/
export function updateFriendlink(id, data) {
return request({
url: `/platform/friendlinks/${id}`,
method: 'put',
data
})
}
/**
* 删除友情链接
* @param {number} id - 链接ID
* @returns {Promise}
*/
export function deleteFriendlink(id) {
return request({
url: `/platform/friendlinks/${id}`,
method: 'delete'
})
}
/**
* 批量删除友情链接
* @param {Array} ids - 链接ID数组
* @returns {Promise}
*/
export function batchDeleteFriendlinks(ids) {
return request({
url: '/platform/friendlinks/batchdelete',
method: 'post',
data: { ids }
})
}
+55
View File
@@ -0,0 +1,55 @@
import request from "@/utils/request";
/**
* 获取所有前端导航
* @returns {Promise}
*/
export function getFrontMenus() {
return request({
url: "/platform/frontmenus",
method: "get",
});
}
/**
* 创建前端导航
* @param {Object} frontMenuData 前端导航数据
* @returns {Promise}
*/
export function createFrontMenu(formData, options = {}) {
return request({
url: "/platform/createfrontmenu",
method: "post",
data: formData,
headers: {
"Content-Type": "multipart/form-data"
}
});
}
/**
* 编辑前端导航
* @param {number|string} id 前端导航ID
* @param {Object} frontMenuData 更新的数据
* @returns {Promise}
*/
export function editFrontMenu(id, frontMenuData) {
return request({
url: `/platform/editfrontmenu/${id}`,
method: "post",
data: frontMenuData,
});
}
/**
* 删除前端导航
* @param {number|string} id 前端导航ID
* @returns {Promise}
*/
export function deleteFrontMenu(id) {
return request({
url: `/platform/deletefrontmenu/${id}`,
method: "delete",
});
}
+21
View File
@@ -0,0 +1,21 @@
import request from '@/utils/request';
/**
* 按天统计号池已提取售卖数量依据 extracted_time
* @param {{ days?: number }} params days 默认 14最大 90
*/
export function getAccountPoolDailyExtract(params) {
return request({
url: '/platform/home/accountPoolDailyExtract',
method: 'get',
params,
});
}
/** 号池账号总数 / 已售卖(按 Cursor、Kiro、Windsurf */
export function getAccountPoolInventoryTotals() {
return request({
url: '/platform/home/accountPoolInventoryTotals',
method: 'get',
});
}
+124
View File
@@ -0,0 +1,124 @@
import request from "@/utils/request";
// 登录(平台端 / 使用租户名称)
export function login(data) {
return request({
url: `/platform/login`,
method: "post",
data,
});
}
/** 当前登录用户信息(含角色名称),需携带 token */
export function getCurrentUser() {
return request({
url: `/platform/currentUser`,
method: "get",
});
}
// 发送登录验证码(手机号)
export function sendLoginCode(data) {
return request({
url: "/platform/sendLoginCode",
method: "post",
data,
});
}
// 手机号验证码登录
export function loginBySms(data) {
return request({
url: "/platform/loginBySms",
method: "post",
data,
});
}
// 登出
export function logout(userInfo = null) {
// 如果没有传入 userInfo,尝试从 localStorage 获取
if (!userInfo) {
const cachedUserInfo = localStorage.getItem('userInfo');
if (cachedUserInfo) {
try {
userInfo = JSON.parse(cachedUserInfo);
} catch (e) {
console.error('Failed to parse userInfo from localStorage:', e);
}
}
}
return request({
url: `/platform/logout`,
method: "post",
data: userInfo ? { userInfo: userInfo } : {},
});
}
/**
* 获取极验3.0数据
* @returns {Promise}
*/
export function getGeetest3Infos() {
return request({
url: '/platform/login/getGeetest3Infos',
method: 'get'
});
}
/**
* 获取极验4.0数据
* @returns {Promise}
*/
export function getGeetest4Infos() {
return request({
url: '/platform/login/getGeetest4Infos',
method: 'get'
});
}
/**
* 判断是否开启验证
* @returns {Promise}
*/
export function getOpenVerify() {
return request({
url: '/platform/login/getOpenVerify',
method: 'get'
});
}
// 忘记密码重置
export function resetPassword(data) {
return request({
url: "/platform/resetPassword",
method: "post",
data,
});
}
// 发送找回密码验证码
export function sendResetCode(data) {
return request({
url: "/platform/sendResetCode",
method: "post",
data,
});
}
// 租户端自助注册(/backend/*,与 go-platform routers/backend 一致)
export function register(data) {
return request({
url: "/backend/register",
method: "post",
data,
});
}
export function sendRegisterCode(data) {
return request({
url: "/backend/sendRegisterCode",
method: "post",
data,
});
}
+53
View File
@@ -0,0 +1,53 @@
import request from "@/utils/request";
// 获取所有菜单
export function getAllMenus(params) {
return request({
url: `/platform/allmenu`,
method: "get",
params,
});
}
//获取用户菜单
export function getMenus(id){
return request({
url: `/platform/menu/${parseInt(id)}`,
method: "get",
});
}
// 更新菜单状态
export function updateMenuStatus(menuId, status) {
return request({
url: `/platform/menu/status/${menuId}`,
method: "patch",
data: { status },
});
}
// 创建菜单
export function createMenu(menuData) {
return request({
url: `/platform/createmenu`,
method: "post",
data: menuData,
});
}
// 更新菜单
export function updateMenu(menuId, menuData) {
return request({
url: `/platform/updatemenu/${menuId}`,
method: "put",
data: menuData,
});
}
// 删除菜单
export function deleteMenu(menuId) {
return request({
url: `/platform/deletemenu/${menuId}`,
method: "delete",
});
}
+57
View File
@@ -0,0 +1,57 @@
import request from "@/utils/request";
/**
* 获取模块中心分类
* @returns {Promise}
*/
export function getModuleCategory() {
return request({
url: "/platform/moduleCategory",
method: "get",
});
}
/**
* 获取模块中心列表
* @param {number} cid 分类id
* @returns {Promise}
*/
export function getModules(cid) {
return request({
url: "/platform/moduleCenter/modules",
method: "get",
params: { cid }
});
}
/**
* 编辑模块分类
* @param {Object} data 分类数据
* @param {number} data.id 分类id编辑时必填新增时不填
* @param {string} data.title 分类名称
* @param {number} data.status 分类状态
* @returns {Promise}
*/
export function editModuleCategory(data) {
return request({
url: "/platform/moduleCenter/editCategory",
method: "post",
data
});
}
/**
* 编辑模块
* @param {Object} data 模块数据
* @param {number} data.id 模块id编辑时必填新增时不填
* @param {string} data.title 模块名称
* @param {number} data.status 模块状态
* @returns {Promise}
*/
export function editModules(data) {
return request({
url: "/platform/moduleCenter/editModules",
method: "post",
data
});
}
+68
View File
@@ -0,0 +1,68 @@
import request from '@/utils/request';
export function getModulesList() {
return request({
url: '/platform/modules/list',
method: 'get',
});
}
export function getTenantList() {
return request({
url: '/platform/modules/getTenantList',
method: 'get',
});
}
export function getModuleDetail(id) {
return request({
url: `/platform/modules/${id}`,
method: 'get',
});
}
export function addModule(data) {
return request({
url: '/platform/modules',
method: 'post',
data,
});
}
export function editModule(id, data) {
return request({
url: `/platform/modules/${id}`,
method: 'put',
data,
});
}
export function deleteModule(id) {
return request({
url: `/platform/modules/${id}`,
method: 'delete',
});
}
export function batchDeleteModules(ids) {
return request({
url: '/platform/modules/batchDelete',
method: 'post',
data: { ids },
});
}
export function changeModuleStatus(id, status) {
return request({
url: '/platform/modules/status',
method: 'post',
data: { id, status },
});
}
export function getModulesSelectList() {
return request({
url: '/platform/modules/select/list',
method: 'get',
});
}
+64
View File
@@ -0,0 +1,64 @@
import request from "@/utils/request";
/**
* 获取所有单页
* @returns {Promise}
*/
export function getOnePages() {
return request({
url: "/platform/allonepages",
method: "get",
});
}
/**
* 创建单页
* @param {Object} onePageData 单页数据
* @returns {Promise}
*/
export function createOnePage(formData) {
return request({
url: "/platform/createonepage",
method: "post",
data: formData,
});
}
/**
* 编辑单页
* @param {number|string} id 单页ID
* @param {Object} onePageData 更新的数据
* @returns {Promise}
*/
export function editOnePage(id, onePageData) {
return request({
url: `/platform/editonepage/${id}`,
method: "post",
data: onePageData,
});
}
/**
* 删除单页
* @param {number|string} id 单页ID
* @returns {Promise}
*/
export function deleteOnePage(id) {
return request({
url: `/platform/deleteonepage/${id}`,
method: "delete",
});
}
/**
* 根据路径获取单页前端使用
* @param {string} path 路由路径
* @returns {Promise}
*/
export function getOnePageByPath(path) {
return request({
url: `/index/onepage/${encodeURIComponent(path)}`,
method: "get",
});
}
+70
View File
@@ -0,0 +1,70 @@
import request from "@/utils/request";
/**
* 获取操作日志列表
* @param {Object} params 查询参数
* @param {number} params.page 页码
* @param {number} params.pageSize 每页数量
* @param {string} params.keyword 关键词搜索
* @param {string} params.module 模块筛选
* @param {string} params.action 操作动作筛选
* @param {string} params.status 状态筛选
* @param {string} params.startTime 开始时间
* @param {string} params.endTime 结束时间
* @returns {Promise}
*/
export function getOperationLogs(params) {
return request({
url: "/platform/operationLogs",
method: "get",
params,
});
}
/**
* 获取操作日志详情
* @param {number|string} id 日志ID
* @returns {Promise}
*/
export function getOperationLogDetail(id) {
return request({
url: `/platform/operationLogs/${id}`,
method: "get",
});
}
/**
* 删除操作日志
* @param {number|string} id 日志ID
* @returns {Promise}
*/
export function deleteOperationLog(id) {
return request({
url: `/platform/operationLogs/${id}`,
method: "delete",
});
}
/**
* 批量删除操作日志
* @param {Array} ids 日志ID数组
* @returns {Promise}
*/
export function batchDeleteOperationLogs(ids) {
return request({
url: "/platform/operationLogs/batchDelete",
method: "post",
data: { ids },
});
}
/**
* 获取操作统计信息
* @returns {Promise}
*/
export function getOperationStatistics() {
return request({
url: "/platform/operationLogs/statistics",
method: "get",
});
}
+24
View File
@@ -0,0 +1,24 @@
import request from '@/utils/request';
export function getAllMenuPermissions(params = {}) {
return request({
url: '/platform/allmenupermissions',
method: 'get',
params
});
}
export function getRolePermissions(roleId) {
return request({
url: `/platform/rolepermissions/${roleId}`,
method: 'get'
});
}
export function assignRolePermissions(roleId, permissions) {
return request({
url: `/platform/assignrolepermissions/${roleId}`,
method: 'post',
data: { permissions }
});
}
+52
View File
@@ -0,0 +1,52 @@
import request from '@/utils/request';
// 获取租户下的所有职位
export function getTenantPositions(tenantId) {
return request({
url: `/platform/positions/tenant/${tenantId}`,
method: 'get',
});
}
// 根据部门ID获取职位列表
export function getPositionsByDepartment(departmentId) {
return request({
url: `/platform/positions/department/${departmentId}`,
method: 'get',
});
}
// 获取职位详情
export function getPositionInfo(positionId) {
return request({
url: `/platform/positions/${positionId}`,
method: 'get',
});
}
// 添加职位
export function addPosition(data) {
return request({
url: '/platform/positions',
method: 'post',
data,
});
}
// 更新职位信息
export function editPosition(positionId, data) {
return request({
url: `/platform/positions/${positionId}`,
method: 'put',
data,
});
}
// 删除职位
export function deletePosition(positionId) {
return request({
url: `/platform/positions/${positionId}`,
method: 'delete',
});
}
+105
View File
@@ -0,0 +1,105 @@
import request from '@/utils/request'
/**
* 获取特色产品列表
* @param {Object} params - 查询参数
* @returns {Promise}
*/
export function getProductsList(params) {
return request({
url: '/platform/productsList',
method: 'get',
params
})
}
/**
* 添加特色产品
* @param {Object} data - 产品数据
* @returns {Promise}
*/
export function addProducts(data) {
return request({
url: '/platform/addProducts',
method: 'post',
data
})
}
/**
* 更新特色产品
* @param {number} id - 产品ID
* @param {Object} data - 产品数据
* @returns {Promise}
*/
export function updateProducts(id, data) {
return request({
url: `/platform/editProducts/${id}`,
method: 'put',
data
})
}
/**
* 删除特色产品
* @param {number} id - 产品ID
* @returns {Promise}
*/
export function deleteProducts(id) {
return request({
url: `/platform/deleteProducts/${id}`,
method: 'delete'
})
}
/**
* 获取产品分类列表
* @param {Object} params - 查询参数
* @returns {Promise}
*/
export function getProductsTypesList(params) {
return request({
url: '/platform/productsTypesList',
method: 'get',
params
})
}
/**
* 添加产品分类
* @param {Object} data - 分类数据
* @returns {Promise}
*/
export function addProductsTypes(data) {
return request({
url: '/platform/addProductsTypes',
method: 'post',
data
})
}
/**
* 更新产品分类
* @param {number} id - 分类ID
* @param {Object} data - 分类数据
* @returns {Promise}
*/
export function updateProductsTypes(id, data) {
return request({
url: `/platform/editProductsTypes/${id}`,
method: 'put',
data
})
}
/**
* 删除产品分类
* @param {number} id - 分类ID
* @returns {Promise}
*/
export function deleteProductsTypes(id) {
return request({
url: `/platform/deleteProductsTypes/${id}`,
method: 'delete'
})
}
+38
View File
@@ -0,0 +1,38 @@
import request from '@/utils/request'
export function getAllRoles() {
return request({
url: '/platform/allRoles',
method: 'get'
})
}
export function getRoleById(id) {
return request({
url: `/platform/roles/${id}`,
method: 'get'
})
}
export function createRole(data) {
return request({
url: '/platform/roles',
method: 'post',
data
})
}
export function updateRole(id, data) {
return request({
url: `/platform/roles/${id}`,
method: 'put',
data
})
}
export function deleteRole(id) {
return request({
url: `/platform/roles/${id}`,
method: 'delete'
})
}
+53
View File
@@ -0,0 +1,53 @@
import request from '@/utils/request'
/**
* 获取特色服务列表
* @param {Object} params - 查询参数
* @returns {Promise}
*/
export function getServiceList(params) {
return request({
url: '/platform/servicesList',
method: 'get',
params
})
}
/**
* 添加特色服务
* @param {Object} data - 服务数据
* @returns {Promise}
*/
export function addService(data) {
return request({
url: '/platform/addServices',
method: 'post',
data
})
}
/**
* 更新特色服务
* @param {number} id - 服务ID
* @param {Object} data - 服务数据
* @returns {Promise}
*/
export function updateService(id, data) {
return request({
url: `/platform/editServices/${id}`,
method: 'put',
data
})
}
/**
* 删除特色服务
* @param {number} id - 服务ID
* @returns {Promise}
*/
export function deleteService(id) {
return request({
url: `/platform/deleteServices/${id}`,
method: 'delete'
})
}
+153
View File
@@ -0,0 +1,153 @@
import request from "@/utils/request";
/**
* 获取基本信息
* @param {number} tid 租户ID
* @returns {Promise}
*/
export function getNormalInfos(tid) {
return request({
url: "/platform/normalInfos",
method: "get",
params: { tid }
});
}
/**
* 保存基本信息
* @param {Object} data 要保存的数据
* @returns {Promise}
*/
export function saveNormalInfos(data) {
return request({
url: "/platform/saveNormalInfos",
method: "post",
data: data,
});
}
/**
* 获取登录验证数据
* @returns {Promise}
*/
export function getVerifyInfos() {
return request({
url: "/platform/loginVerifyInfos",
method: "get",
});
}
/**
* 保存登录验证数据
* @param {Object} data 要保存的数据
* @returns {Promise}
*/
export function saveVerifyInfos(data) {
return request({
url: "/platform/saveloginVerifyInfos",
method: "post",
data: data,
});
}
/**
* 获取法律声明和隐私条款
* @param {number} tid 租户ID
* @returns {Promise}
*/
export function getLegalInfos(tid) {
return request({
url: "/platform/legalInfos",
method: "get",
params: { tid }
});
}
/**
* 保存法律声明和隐私条款
* @param {Object} data 要保存的数据
* @returns {Promise}
*/
export function saveLegalInfos(data) {
return request({
url: "/platform/saveLegalInfos",
method: "post",
data: data,
});
}
/**
* 获取企业信息
* @param {number} tid 租户ID
* @returns {Promise}
*/
export function getCompanyInfos(tid) {
return request({
url: "/platform/companyInfos",
method: "get",
params: { tid }
});
}
/**
* 保存企业信息
* @param {Object} data 要保存的数据
* @returns {Promise}
*/
export function saveCompanyInfos(data) {
return request({
url: "/platform/saveCompanyInfos",
method: "post",
data: data,
});
}
/**
* 获取企业SEO
* @param {number} tid 租户ID
* @returns {Promise}
*/
export function getCompanySeo(tid) {
return request({
url: "/platform/companySeo",
method: "get",
params: { tid }
});
}
/**
* 保存企业SEO
* @param {Object} data 要保存的数据
* @returns {Promise}
*/
export function saveCompanySeo(data) {
return request({
url: "/platform/saveCompanySeo",
method: "post",
data: data,
});
}
/**
* 获取存储配置
* @returns {Promise}
*/
export function getStorageConfig() {
return request({
url: "/platform/storageConfig",
method: "get",
});
}
/**
* 保存存储配置
* @param {Object} data 要保存的数据
* @returns {Promise}
*/
export function saveStorageConfig(data) {
return request({
url: "/platform/saveStorageConfig",
method: "post",
data: data,
});
}
+56
View File
@@ -0,0 +1,56 @@
import request from "@/utils/request";
/**
*
*/
export function getSmsInfo() {
return request({
url: "/platform/sms/info",
method: "get",
});
}
/**
*
*/
export function editSmsInfo(data: any) {
return request({
url: "/platform/sms/editinfo",
method: "post",
data,
});
}
/**
*
*/
export function sendTestSms(data: any) {
return request({
url: "/platform/sms/sendtest",
method: "post",
data,
});
}
/**
*
*/
export function getSmsTaskList(params: { status?: string | number; phone?: string } = {}) {
return request({
url: "/platform/sms/taskList",
method: "get",
params,
});
}
/**
*
*/
export function editSmsTask(id: number | string, data: any) {
return request({
url: `/platform/sms/taskEdit/${id}`,
method: "post",
data,
});
}
+39
View File
@@ -0,0 +1,39 @@
import request from "@/utils/request";
export function getSoftwareUpgradeList(params) {
return request({
url: "/platform/softwareupgrade/list",
method: "get",
params,
});
}
export function getSoftwareUpgradeDetail(id) {
return request({
url: `/platform/softwareupgrade/${id}`,
method: "get",
});
}
export function createSoftwareUpgrade(data) {
return request({
url: "/platform/softwareupgrade",
method: "post",
data,
});
}
export function updateSoftwareUpgrade(id, data) {
return request({
url: `/platform/softwareupgrade/${id}`,
method: "post",
data,
});
}
export function deleteSoftwareUpgrade(id) {
return request({
url: `/platform/softwareupgrade/${id}`,
method: "delete",
});
}
+84
View File
@@ -0,0 +1,84 @@
import request from "@/utils/request";
/*************************************************
****************** 租户相关接口 ******************
*************************************************/
/**
* 获取租户列表
* @param {Object} params 包含 page pageSize
* @returns {Promise}
*/
export function getTenantList(params) {
return request({
url: "/platform/tenant/getTenant",
method: "get",
params: params,
});
}
/**
* 获取租户详情
* @param {number} id 租户ID
* @returns {Promise}
*/
export function getTenantDetail(id) {
return request({
url: `/platform/tenant/getTenantDetail/${id}`,
method: "get",
});
}
/**
* 创建租户数据
* @param {Object} data 租户数据
* @returns {Promise}
*/
export function createTenant(data) {
return request({
url: "/platform/tenant/createTenant",
method: "post",
data: data,
headers: {
"Content-Type": "multipart/form-data",
},
});
}
/**
* 更新租户数据
* @param {Object} data 租户数据
* @returns {Promise}
*/
export function editTenant(id, data) {
return request({
url: `/platform/tenant/editTenant/${id}`,
method: "post",
data: data,
});
}
/**
* 删除租户数据
* @param {number} id 租户ID
* @returns {Promise}
*/
export function deleteTenant(id) {
return request({
url: `/platform/tenant/deleteTenant/${id}`,
method: "delete",
});
}
/**
* 校验租户编码是否重复
* @param {string} tenant_code 编码
* @param {number} id 可选当前编辑的租户ID
*/
export function checkTenantCode(tenant_code) {
return request({
url: '/platform/tenant/findTenantCode',
method: 'get',
params: { tenant_code }
});
}
+29
View File
@@ -0,0 +1,29 @@
import request from "@/utils/request";
/** 获取租户用户列表;params 可含 tid、uid、keyword(模糊匹配姓名/手机/邮箱/账号) */
export function getTenantUserList(params) {
return request({
url: "/platform/tenantUser/list",
method: "get",
params,
});
}
/** 创建租户用户绑定(后端写入 yz_tenant_user */
export function createTenantUser(data) {
return request({
url: "/platform/tenantUser/create",
method: "post",
data,
});
}
// 修改租户用户绑定信息(用于修改密码等)
export function editTenantUser(id, data) {
return request({
url: `/platform/tenantUser/edit/${id}`,
method: "post",
data,
});
}
+36
View File
@@ -0,0 +1,36 @@
import request from '@/utils/request'
// 获取模板列表
export function getThemeList() {
return request({
url: '/platform/theme',
method: 'get'
})
}
// 切换模板
export function switchTheme(data) {
return request({
url: '/platform/theme/switch',
method: 'post',
data
})
}
// 获取模板数据
export function getThemeData(params) {
return request({
url: '/platform/theme/data',
method: 'get',
params
})
}
// 保存模板数据
export function saveThemeData(data) {
return request({
url: '/platform/theme/data',
method: 'post',
data
})
}
+68
View File
@@ -0,0 +1,68 @@
import request from '@/utils/request';
//获取所有用户信息
export function getAllUsers() {
return request({
url: '/platform/getAllUsers',
method: 'get',
});
}
//获取租户用户
export function getTenantUsers(tenantId) {
return request({
url: `/platform/getTenantUsers/${tenantId}`,
method: 'get',
});
}
// 获取用户信息
export function getUserInfo(userId) {
return request({
url: `/platform/getUserInfo/${userId}`,
method: 'get',
});
}
// 添加用户
export function addUser(data) {
return request({
url: '/platform/addUser',
method: 'post',
data,
});
}
// 编辑用户信息
export function editUser(userId, data) {
return request({
url: `/platform/editUser/${userId}`,
method: 'post',
data,
});
}
// 更新用户信息(编辑用户的别名)
export function updateUserInfo(userId, data) {
return editUser(userId, data);
}
// 删除用户
export function deleteUser(userId) {
return request({
url: `/platform/deleteUser/${userId}`,
method: 'delete',
});
}
// 修改密码
export function changePassword(userId, data) {
return request({
url: '/platform/changePassword',
method: 'post',
data: {
id: userId,
password: data.newPassword
},
});
}
+11
View File
@@ -0,0 +1,11 @@
// 文章管理相关API
import request from "@/utils/request";
// 获取文章列表
export function GetCRMWorkbench(params) {
return request({
url: `/platform/workbench/crm`,
method: "get",
params,
});
}
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

File diff suppressed because one or more lines are too long
+353
View File
@@ -0,0 +1,353 @@
"v0.5.0 Geetest Inc.";
(function (window) {
"use strict";
if (typeof window === 'undefined') {
throw new Error('Geetest requires browser environment');
}
var document = window.document;
var Math = window.Math;
var head = document.getElementsByTagName("head")[0];
function _Object(obj) {
this._obj = obj;
}
_Object.prototype = {
_each: function (process) {
var _obj = this._obj;
for (var k in _obj) {
if (_obj.hasOwnProperty(k)) {
process(k, _obj[k]);
}
}
return this;
}
};
function Config(config) {
var self = this;
new _Object(config)._each(function (key, value) {
self[key] = value;
});
}
Config.prototype = {
api_server: 'api.geetest.com',
protocol: 'http://',
typePath: '/gettype.php',
fallback_config: {
slide: {
static_servers: ["static.geetest.com", "static.geevisit.com"],
type: 'slide',
slide: '/static/js/geetest.0.0.0.js'
},
fullpage: {
static_servers: ["static.geetest.com", "static.geevisit.com"],
type: 'fullpage',
fullpage: '/static/js/fullpage.0.0.0.js'
}
},
_get_fallback_config: function () {
var self = this;
if (isString(self.type)) {
return self.fallback_config[self.type];
} else if (self.new_captcha) {
return self.fallback_config.fullpage;
} else {
return self.fallback_config.slide;
}
},
_extend: function (obj) {
var self = this;
new _Object(obj)._each(function (key, value) {
self[key] = value;
})
}
};
var isNumber = function (value) {
return (typeof value === 'number');
};
var isString = function (value) {
return (typeof value === 'string');
};
var isBoolean = function (value) {
return (typeof value === 'boolean');
};
var isObject = function (value) {
return (typeof value === 'object' && value !== null);
};
var isFunction = function (value) {
return (typeof value === 'function');
};
var MOBILE = /Mobi/i.test(navigator.userAgent);
var pt = MOBILE ? 3 : 0;
var callbacks = {};
var status = {};
var nowDate = function () {
var date = new Date();
var year = date.getFullYear();
var month = date.getMonth() + 1;
var day = date.getDate();
var hours = date.getHours();
var minutes = date.getMinutes();
var seconds = date.getSeconds();
if (month >= 1 && month <= 9) {
month = '0' + month;
}
if (day >= 0 && day <= 9) {
day = '0' + day;
}
if (hours >= 0 && hours <= 9) {
hours = '0' + hours;
}
if (minutes >= 0 && minutes <= 9) {
minutes = '0' + minutes;
}
if (seconds >= 0 && seconds <= 9) {
seconds = '0' + seconds;
}
var currentdate = year + '-' + month + '-' + day + " " + hours + ":" + minutes + ":" + seconds;
return currentdate;
}
var random = function () {
return parseInt(Math.random() * 10000) + (new Date()).valueOf();
};
var loadScript = function (url, cb) {
var script = document.createElement("script");
script.charset = "UTF-8";
script.async = true;
// 对geetestçš„é™æ€èµ„æºæ·»åŠ crossOrigin
if ( /static\.geetest\.com/g.test(url)) {
script.crossOrigin = "anonymous";
}
script.onerror = function () {
cb(true);
};
var loaded = false;
script.onload = script.onreadystatechange = function () {
if (!loaded &&
(!script.readyState ||
"loaded" === script.readyState ||
"complete" === script.readyState)) {
loaded = true;
setTimeout(function () {
cb(false);
}, 0);
}
};
script.src = url;
head.appendChild(script);
};
var normalizeDomain = function (domain) {
// special domain: uems.sysu.edu.cn/jwxt/geetest/
// return domain.replace(/^https?:\/\/|\/.*$/g, ''); uems.sysu.edu.cn
return domain.replace(/^https?:\/\/|\/$/g, ''); // uems.sysu.edu.cn/jwxt/geetest
};
var normalizePath = function (path) {
path = path.replace(/\/+/g, '/');
if (path.indexOf('/') !== 0) {
path = '/' + path;
}
return path;
};
var normalizeQuery = function (query) {
if (!query) {
return '';
}
var q = '?';
new _Object(query)._each(function (key, value) {
if (isString(value) || isNumber(value) || isBoolean(value)) {
q = q + encodeURIComponent(key) + '=' + encodeURIComponent(value) + '&';
}
});
if (q === '?') {
q = '';
}
return q.replace(/&$/, '');
};
var makeURL = function (protocol, domain, path, query) {
domain = normalizeDomain(domain);
var url = normalizePath(path) + normalizeQuery(query);
if (domain) {
url = protocol + domain + url;
}
return url;
};
var load = function (config, send, protocol, domains, path, query, cb) {
var tryRequest = function (at) {
var url = makeURL(protocol, domains[at], path, query);
loadScript(url, function (err) {
if (err) {
if (at >= domains.length - 1) {
cb(true);
// report gettype error
if (send) {
config.error_code = 508;
var url = protocol + domains[at] + path;
reportError(config, url);
}
} else {
tryRequest(at + 1);
}
} else {
cb(false);
}
});
};
tryRequest(0);
};
var jsonp = function (domains, path, config, callback) {
if (isObject(config.getLib)) {
config._extend(config.getLib);
callback(config);
return;
}
if (config.offline) {
callback(config._get_fallback_config());
return;
}
var cb = "geetest_" + random();
window[cb] = function (data) {
if (data.status == 'success') {
callback(data.data);
} else if (!data.status) {
callback(data);
} else {
callback(config._get_fallback_config());
}
window[cb] = undefined;
try {
delete window[cb];
} catch (e) {
}
};
load(config, true, config.protocol, domains, path, {
gt: config.gt,
callback: cb
}, function (err) {
if (err) {
callback(config._get_fallback_config());
}
});
};
var reportError = function (config, url) {
load(config, false, config.protocol, ['monitor.geetest.com'], '/monitor/send', {
time: nowDate(),
captcha_id: config.gt,
challenge: config.challenge,
pt: pt,
exception_url: url,
error_code: config.error_code
}, function (err) {})
}
var throwError = function (errorType, config) {
var errors = {
networkError: '网络错误',
gtTypeError: 'gt字段不是字符串类型'
};
if (typeof config.onError === 'function') {
config.onError(errors[errorType]);
} else {
throw new Error(errors[errorType]);
}
};
var detect = function () {
return window.Geetest || document.getElementById("gt_lib");
};
if (detect()) {
status.slide = "loaded";
}
window.initGeetest = function (userConfig, callback) {
var config = new Config(userConfig);
if (userConfig.https) {
config.protocol = 'https://';
} else if (!userConfig.protocol) {
config.protocol = window.location.protocol + '//';
}
// for KFC
if (userConfig.gt === '050cffef4ae57b5d5e529fea9540b0d1' ||
userConfig.gt === '3bd38408ae4af923ed36e13819b14d42') {
config.apiserver = 'yumchina.geetest.com/'; // for old js
config.api_server = 'yumchina.geetest.com';
}
if(userConfig.gt){
window.GeeGT = userConfig.gt
}
if(userConfig.challenge){
window.GeeChallenge = userConfig.challenge
}
if (isObject(userConfig.getType)) {
config._extend(userConfig.getType);
}
jsonp((config.api_server_v3 || [config.api_server || config.apiserver]), config.typePath, config, function (newConfig) {
var type = newConfig.type;
var init = function () {
config._extend(newConfig);
callback(new window.Geetest(config));
};
callbacks[type] = callbacks[type] || [];
var s = status[type] || 'init';
if (s === 'init') {
status[type] = 'loading';
callbacks[type].push(init);
load(config, true, config.protocol, newConfig.static_servers || newConfig.domains, newConfig[type] || newConfig.path, null, function (err) {
if (err) {
status[type] = 'fail';
throwError('networkError', config);
} else {
status[type] = 'loaded';
var cbs = callbacks[type];
for (var i = 0, len = cbs.length; i < len; i = i + 1) {
var cb = cbs[i];
if (isFunction(cb)) {
cb();
}
}
callbacks[type] = [];
}
});
} else if (s === "loaded") {
init();
} else if (s === "fail") {
throwError('networkError', config);
} else if (s === "loading") {
callbacks[type].push(init);
}
});
};
})(window);
+487
View File
@@ -0,0 +1,487 @@
"v4.2.0 Geetest Inc.";
(function (window) {
"use strict";
if (typeof window === 'undefined') {
throw new Error('Geetest requires browser environment');
}
var document = window.document;
var Math = window.Math;
var head = document.getElementsByTagName("head")[0];
var TIMEOUT = 10000;
function _Object(obj) {
this._obj = obj;
}
_Object.prototype = {
_each: function (process) {
var _obj = this._obj;
for (var k in _obj) {
if (_obj.hasOwnProperty(k)) {
process(k, _obj[k]);
}
}
return this;
},
_extend: function (obj){
var self = this;
new _Object(obj)._each(function (key, value){
self._obj[key] = value;
})
}
};
var uuid = function () {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0;
var v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
};
function Config(config) {
var self = this;
new _Object(config)._each(function (key, value) {
self[key] = value;
});
}
Config.prototype = {
apiServers: ['gcaptcha4.geetest.com','gcaptcha4.geevisit.com','gcaptcha4.gsensebot.com'],
staticServers: ["static.geetest.com",'static.geevisit.com'],
protocol: 'http://',
typePath: '/load',
fallback_config: {
bypass: {
staticServers: ["static.geetest.com",'static.geevisit.com'],
type: 'bypass',
bypass: '/v4/bypass.js'
}
},
_get_fallback_config: function () {
var self = this;
if (isString(self.type)) {
return self.fallback_config[self.type];
} else {
return self.fallback_config.bypass;
}
},
_extend: function (obj) {
var self = this;
new _Object(obj)._each(function (key, value) {
self[key] = value;
})
}
};
var isNumber = function (value) {
return (typeof value === 'number');
};
var isString = function (value) {
return (typeof value === 'string');
};
var isBoolean = function (value) {
return (typeof value === 'boolean');
};
var isObject = function (value) {
return (typeof value === 'object' && value !== null);
};
var isFunction = function (value) {
return (typeof value === 'function');
};
var MOBILE = /Mobi/i.test(navigator.userAgent);
var callbacks = {};
var status = {};
var random = function () {
return parseInt(Math.random() * 10000) + (new Date()).valueOf();
};
// bind 函数polify, 不带new功能的bind
var bind = function(target,context){
if(typeof target !== 'function'){
return;
}
var args = Array.prototype.slice.call(arguments,2);
if(Function.prototype.bind){
return target.bind(context, args);
}else {
return function(){
var _args = Array.prototype.slice.call(arguments);
return target.apply(context,args.concat(_args));
}
}
}
var toString = Object.prototype.toString;
var _isFunction = function(obj) {
return typeof(obj) === 'function';
};
var _isObject = function(obj) {
return obj === Object(obj);
};
var _isArray = function(obj) {
return toString.call(obj) == '[object Array]';
};
var _isDate = function(obj) {
return toString.call(obj) == '[object Date]';
};
var _isRegExp = function(obj) {
return toString.call(obj) == '[object RegExp]';
};
var _isBoolean = function(obj) {
return toString.call(obj) == '[object Boolean]';
};
function resolveKey(input){
return input.replace(/(\S)(_([a-zA-Z]))/g, function(match, $1, $2, $3){
return $1 + $3.toUpperCase() || "";
})
}
function camelizeKeys(input, convert){
if(!_isObject(input) || _isDate(input) || _isRegExp(input) || _isBoolean(input) || _isFunction(input)){
return convert ? resolveKey(input) : input;
}
if(_isArray(input)){
var temp = [];
for(var i = 0; i < input.length; i++){
temp.push(camelizeKeys(input[i]));
}
}else {
var temp = {};
for(var prop in input){
if(input.hasOwnProperty(prop)){
temp[camelizeKeys(prop, true)] = camelizeKeys(input[prop]);
}
}
}
return temp;
}
var loadScript = function (url, cb, timeout) {
var script = document.createElement("script");
script.charset = "UTF-8";
script.async = true;
// 对geetestçš„é™æ€èµ„æºæ·»åŠ crossOrigin
if ( /static\.geetest\.com/g.test(url)) {
script.crossOrigin = "anonymous";
}
script.onerror = function () {
cb(true);
// 错误触发了,超时逻辑就不用了
loaded = true;
};
var loaded = false;
script.onload = script.onreadystatechange = function () {
if (!loaded &&
(!script.readyState ||
"loaded" === script.readyState ||
"complete" === script.readyState)) {
loaded = true;
setTimeout(function () {
cb(false);
}, 0);
}
};
script.src = url;
head.appendChild(script);
setTimeout(function () {
if (!loaded) {
script.onerror = script.onload = null;
script.remove && script.remove();
cb(true);
}
}, timeout || TIMEOUT);
};
var normalizeDomain = function (domain) {
// special domain: uems.sysu.edu.cn/jwxt/geetest/
// return domain.replace(/^https?:\/\/|\/.*$/g, ''); uems.sysu.edu.cn
return domain.replace(/^https?:\/\/|\/$/g, ''); // uems.sysu.edu.cn/jwxt/geetest
};
var normalizePath = function (path) {
path = path && path.replace(/\/+/g, '/');
if (path.indexOf('/') !== 0) {
path = '/' + path;
}
return path;
};
var normalizeQuery = function (query) {
if (!query) {
return '';
}
var q = '?';
new _Object(query)._each(function (key, value) {
if (isString(value) || isNumber(value) || isBoolean(value)) {
q = q + encodeURIComponent(key) + '=' + encodeURIComponent(value) + '&';
}
});
if (q === '?') {
q = '';
}
return q.replace(/&$/, '');
};
var makeURL = function (protocol, domain, path, query) {
domain = normalizeDomain(domain);
var url = normalizePath(path) + normalizeQuery(query);
if (domain) {
url = protocol + domain + url;
}
return url;
};
var load = function (config, protocol, domains, path, query, cb, handleCb) {
var tryRequest = function (at) {
// 处理jsonp回调,这里为了保证每个不同jsonp都有唯一的回调函数
if(handleCb){
var cbName = "geetest_" + random();
// 需要与预先定义好cbnameå‚æ•°ï¼Œåˆ é™¤å¯¹è±¡
window[cbName] = bind(handleCb, null, cbName);
query.callback = cbName;
}
var url = makeURL(protocol, domains[at], path, query);
loadScript(url, function (err) {
if (err) {
// 超时或者出错的时候 移除回调
if(cbName){
try {
window[cbName] = function(){
window[cbName] = null;
}
} catch (e) {}
}
if (at >= domains.length - 1) {
cb(true);
// report gettype error
} else {
tryRequest(at + 1);
}
} else {
cb(false);
}
}, config.timeout);
};
tryRequest(0);
};
var jsonp = function (domains, path, config, callback) {
var handleCb = function (cbName, data) {
// 保证只执行一次,全部超时的情况下不会再触发;
if (data.status == 'success') {
callback(data.data);
} else if (!data.status) {
callback(data);
} else {
//接口有返回,但是返回了错误状态,进入报错逻辑
callback(data);
}
window[cbName] = undefined;
try {
delete window[cbName];
} catch (e) {
}
};
load(config, config.protocol, domains, path, {
callback: '',
captcha_id: config.captchaId,
challenge: config.challenge || uuid(),
client_type: config.clientType ? config.clientType : (MOBILE? 'h5':'web'),
risk_type: config.riskType,
user_info: config.userInfo,
call_type: config.callType,
lang: config.language? config.language : navigator.appName === 'Netscape' ? navigator.language.toLowerCase() : navigator.userLanguage.toLowerCase()
}, function (err) {
// ç½‘ç»œé—®é¢˜æŽ¥å£æ²¡æœ‰è¿”å›žï¼Œç›´æŽ¥ä½¿ç”¨æœ¬åœ°éªŒè¯ç ï¼Œèµ°å®•æœºæ¨¡å¼
// è¿™é‡Œå¯ä»¥æ·»åŠ ç”¨æˆ·çš„é€»è¾‘
if(err && typeof config.offlineCb === 'function'){
// 执行自己的宕机
config.offlineCb();
return;
}
if(err){
callback(config._get_fallback_config());
}
}, handleCb);
};
var reportError = function (config, url) {
load(config, config.protocol, ['monitor.geetest.com'], '/monitor/send', {
time: Date.now().getTime(),
captcha_id: config.gt,
challenge: config.challenge,
exception_url: url,
error_code: config.error_code
}, function (err) {})
}
var throwError = function (errorType, config, errObj) {
var errors = {
networkError: '网络错误',
gtTypeError: 'gt字段不是字符串类型'
};
if (typeof config.onError === 'function') {
config.onError({
desc: errObj.desc,
msg: errObj.msg,
code: errObj.code
});
} else {
throw new Error(errors[errorType]);
}
};
var detect = function () {
return window.Geetest || document.getElementById("gt_lib");
};
if (detect()) {
status.slide = "loaded";
}
var GeetestIsLoad = function (fname) {
var GeetestIsLoad = false;
var tags = { js: 'script', css: 'link' };
var tagname = fname && tags[fname.split('.').pop()];
if (tagname !== undefined) {
var elts = document.getElementsByTagName(tagname);
for (var i in elts) {
if ((elts[i].href && elts[i].href.toString().indexOf(fname) > 0)
|| (elts[i].src && elts[i].src.toString().indexOf(fname) > 0)) {
GeetestIsLoad = true;
}
}
}
return GeetestIsLoad;
};
window.initGeetest4 = function (userConfig,callback) {
var config = new Config(userConfig);
if (userConfig.https) {
config.protocol = 'https://';
} else if (!userConfig.protocol) {
config.protocol = window.location.protocol + '//';
}
if (isObject(userConfig.getType)) {
config._extend(userConfig.getType);
}
jsonp(config.apiServers , config.typePath, config, function (newConfig) {
//错误捕获,第一个load请求可能直接报错
var newConfig = camelizeKeys(newConfig);
if(newConfig.status === 'error'){
return throwError('networkError', config, newConfig);
}
var type = newConfig.type;
if(config.debug){
new _Object(newConfig)._extend(config.debug)
}
var init = function () {
config._extend(newConfig);
callback(new window.Geetest4(config));
};
callbacks[type] = callbacks[type] || [];
var s = status[type] || 'init';
if (s === 'init') {
status[type] = 'loading';
callbacks[type].push(init);
if(newConfig.gctPath){
load(config, config.protocol, Object.hasOwnProperty.call(config, 'staticServers') ? config.staticServers : newConfig.staticServers || config.staticServers , newConfig.gctPath, null, function (err){
if(err){
throwError('networkError', config, {
code: '60205',
msg: 'Network failure',
desc: {
detail: 'gct resource load timeout'
}
});
}
})
}
load(config, config.protocol, Object.hasOwnProperty.call(config, 'staticServers') ? config.staticServers : newConfig.staticServers || config.staticServers, newConfig.bypass || (newConfig.staticPath + newConfig.js), null, function (err) {
if (err) {
status[type] = 'fail';
throwError('networkError', config, {
code: '60204',
msg: 'Network failure',
desc: {
detail: 'js resource load timeout'
}
});
} else {
status[type] = 'loaded';
var cbs = callbacks[type];
for (var i = 0, len = cbs.length; i < len; i = i + 1) {
var cb = cbs[i];
if (isFunction(cb)) {
cb();
}
}
callbacks[type] = [];
status[type] = 'init';
}
});
} else if (s === "loaded") {
// 判断gctæ˜¯å¦éœ€è¦é‡æ–°åŠ è½½
if(newConfig.gctPath && !GeetestIsLoad(newConfig.gctPath)){
load(config, config.protocol, Object.hasOwnProperty.call(config, 'staticServers') ? config.staticServers : newConfig.staticServers || config.staticServers , newConfig.gctPath, null, function (err){
if(err){
throwError('networkError', config, {
code: '60205',
msg: 'Network failure',
desc: {
detail: 'gct resource load timeout'
}
});
}
})
}
return init();
} else if (s === "fail") {
throwError('networkError', config, {
code: '60204',
msg: 'Network failure',
desc: {
detail: 'js resource load timeout'
}
});
} else if (s === "loading") {
callbacks[type].push(init);
}
});
};
})(window);
+2
View File
@@ -0,0 +1,2 @@
@import './reset.less';
@import './style.less';
+192
View File
@@ -0,0 +1,192 @@
// reset.less - 现代 CSS 样式重置
// 统一盒模型为 border-box
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
// 基础字体与颜色设置
html {
// 基础字体大小 (1rem = 16px)
font-size: 16px;
// 平滑滚动
scroll-behavior: smooth;
height: 100%;
width: 100%;
}
body {
// 继承父级字体设置
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
font-size: 1rem;
line-height: 1.5; // 舒适行高
color: #333; // 基础文本色
// background-color: #fff; // 基础背景色
-webkit-text-size-adjust: 100%; // 防止iOS横屏字体放大
height: 100%;
width: 100%;
margin: 0;
padding: 0;
}
// 移除默认边框
img,
iframe,
embed,
object,
video {
border: 0;
}
// 图片与媒体元素自适应
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
max-width: 100%;
height: auto;
}
// 表格重置
table {
border-collapse: collapse;
border-spacing: 0;
width: 100%;
}
// 列表样式重置
ul,
ol,
li {
list-style: none;
}
// 文本元素重置
a {
color: inherit; // 继承父级颜色
text-decoration: none;
background-color: transparent;
}
a:hover,
a:focus {
outline: none;
}
// 标题元素重置
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
margin: 0;
}
// 表单元素重置
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
font-size: 100%;
line-height: 1.15;
margin: 0;
padding: 0;
border: none;
background: transparent;
color: inherit;
}
button,
input {
overflow: visible;
}
button,
select {
text-transform: none;
}
// 按钮样式重置
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
cursor: pointer;
}
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
// 输入框聚焦样式
input:focus,
select:focus,
textarea:focus,
button:focus {
outline: none;
}
// 文本区域不允许拖拽调整大小
textarea {
overflow: auto;
resize: vertical; // 仅允许垂直调整
}
// 移除占位符默认样式
::-webkit-input-placeholder {
color: #999;
opacity: 1;
}
::-moz-placeholder {
color: #999;
opacity: 1;
}
:-ms-input-placeholder {
color: #999;
opacity: 1;
}
::placeholder {
color: #999;
opacity: 1;
}
// 清除浮动
.clearfix::after {
content: "";
display: table;
clear: both;
}
// 隐藏元素(屏幕阅读器可见)
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
+432
View File
@@ -0,0 +1,432 @@
// Element Plus Message z-index
:root {
--el-message-z-index: 9999;
}
// body 样式
body {
// background-color: #f5f7fa;
color: #303133;
transition: background-color 0.3s ease, color 0.3s ease;
}
// container-box 样式
.container-box {
// background-color: #ffffff;
// border: 1px solid #ebeef5;
padding: 24px;
background-color: var(--el-bg-color);
border: 1px solid var(--el-border-color-lighter);
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.04);
border-radius: 8px;
padding: 24px;
transition: background-color 0.3s, border-color 0.3s, box-shadow 0.3s;
}
.header-bar {
display: flex;
align-items: center;
justify-content: space-between;
}
.pagination-bar {
display: flex;
justify-content: flex-end;
margin: 14px 0 0 0;
}
// 修复 ElMessage 显示问题
// 只修复定位,保持 Element Plus 官方样式
.el-message {
// 确保消息固定在页面顶部中央,不受父容器影响
position: fixed !important;
z-index: var(--el-message-z-index, 9999) !important;
pointer-events: auto !important;
}
.wang-editor-wrapper{
border: 1px solid #dcdfe6 !important;
.toolbar-container{
border-bottom: 1px solid #dcdfe6 !important;
}
.editor-container {
background-color: #ffffff !important;
}
:deep(.w-e-text),
:deep(.w-e-text-container) {
background-color: transparent !important;
* {
color: #1a1a2e !important;
}
p {
color: #1a1a2e !important;
margin: 8px 0 !important;
line-height: 1.8 !important;
font-size: 14px !important;
text-indent: 0 !important;
}
span {
color: #1a1a2e !important;
font-size: 14px !important;
line-height: 1.8 !important;
}
strong, b {
font-weight: 600 !important;
color: #1a1a2e !important;
}
em, i {
font-style: italic !important;
}
u {
text-decoration: underline !important;
}
s, del {
text-decoration: line-through !important;
}
h1 {
color: #1a1a2e !important;
font-size: 28px !important;
font-weight: 600 !important;
margin: 16px 0 12px !important;
line-height: 1.4 !important;
}
h2 {
color: #1a1a2e !important;
font-size: 24px !important;
font-weight: 600 !important;
margin: 16px 0 12px !important;
line-height: 1.4 !important;
}
h3 {
color: #1a1a2e !important;
font-size: 20px !important;
font-weight: 600 !important;
margin: 16px 0 12px !important;
line-height: 1.4 !important;
}
h4 {
color: #1a1a2e !important;
font-size: 18px !important;
font-weight: 600 !important;
margin: 16px 0 12px !important;
line-height: 1.4 !important;
}
h5 {
color: #1a1a2e !important;
font-size: 16px !important;
font-weight: 600 !important;
margin: 16px 0 12px !important;
line-height: 1.4 !important;
}
h6 {
color: #1a1a2e !important;
font-size: 14px !important;
font-weight: 600 !important;
margin: 16px 0 12px !important;
line-height: 1.4 !important;
}
a {
color: #3973ff !important;
text-decoration: underline !important;
&:hover {
color: #3973ff !important;
opacity: 0.8 !important;
}
}
code {
background-color: #f5f7fa !important;
color: #1a1a2e !important;
border: 1px solid #e4e7ed !important;
border-radius: 3px !important;
padding: 2px 6px !important;
font-family: 'Consolas', 'Monaco', monospace !important;
font-size: 13px !important;
}
pre {
background-color: #f5f7fa !important;
border: 1px solid #e4e7ed !important;
border-radius: 4px !important;
color: #1a1a2e !important;
padding: 12px 16px !important;
margin: 12px 0 !important;
overflow-x: auto;
code {
background-color: transparent !important;
border: none !important;
padding: 0 !important;
color: #1a1a2e !important;
font-size: 13px !important;
line-height: 1.6 !important;
}
}
blockquote {
border-left: 4px solid #3973ff !important;
background-color: #f5f7fa !important;
color: #606266 !important;
padding: 8px 16px !important;
margin: 12px 0 !important;
}
table {
border-collapse: collapse !important;
border: 1px solid #e4e7ed !important;
width: 100% !important;
th, td {
border: 1px solid #e4e7ed !important;
background-color: #ffffff !important;
color: #1a1a2e !important;
padding: 8px 12px !important;
min-width: 60px;
}
th {
background-color: #f5f7fa !important;
font-weight: 600 !important;
}
}
ul {
list-style-type: disc !important;
color: #1a1a2e !important;
padding-left: 24px !important;
margin: 8px 0 !important;
}
ol {
list-style-type: decimal !important;
color: #1a1a2e !important;
padding-left: 24px !important;
margin: 8px 0 !important;
}
li {
color: #1a1a2e !important;
line-height: 1.8 !important;
margin: 4px 0 !important;
}
hr {
border-top: 1px solid #e4e7ed !important;
margin: 16px 0 !important;
}
img {
max-width: 100% !important;
border-radius: 4px !important;
margin: 8px 0 !important;
}
video {
max-width: 100% !important;
border-radius: 4px !important;
margin: 8px 0 !important;
}
.w-e-panel-tab-content {
color: #1a1a2e !important;
}
}
}
html.dark {
.wang-editor-wrapper {
border-color: #3d3d3d !important;
background-color: #1a1a1a !important;
.toolbar-container {
background-color: #2d2d2d !important;
border-color: #3d3d3d !important;
}
.editor-container {
background-color: #1a1a1a !important;
&::-webkit-scrollbar-thumb {
background: #4d4d4d !important;
}
&::-webkit-scrollbar-track {
background: #2d2d2d !important;
}
}
:deep(.w-e-text),
:deep(.w-e-text-container) {
background-color: transparent !important;
* {
color: #e0e0e0 !important;
}
p {
color: #e0e0e0 !important;
margin: 8px 0 !important;
line-height: 1.8 !important;
font-size: 14px !important;
text-indent: 0 !important;
}
span {
color: #e0e0e0 !important;
font-size: 14px !important;
line-height: 1.8 !important;
}
strong, b {
font-weight: 600 !important;
color: #e0e0e0 !important;
}
em, i {
font-style: italic !important;
}
u {
text-decoration: underline !important;
}
s, del {
text-decoration: line-through !important;
}
h1 {
color: #e0e0e0 !important;
font-size: 28px !important;
font-weight: 600 !important;
margin: 16px 0 12px !important;
line-height: 1.4 !important;
}
h2 {
color: #e0e0e0 !important;
font-size: 24px !important;
font-weight: 600 !important;
margin: 16px 0 12px !important;
line-height: 1.4 !important;
}
h3 {
color: #e0e0e0 !important;
font-size: 20px !important;
font-weight: 600 !important;
margin: 16px 0 12px !important;
line-height: 1.4 !important;
}
h4 {
color: #e0e0e0 !important;
font-size: 18px !important;
font-weight: 600 !important;
margin: 16px 0 12px !important;
line-height: 1.4 !important;
}
h5 {
color: #e0e0e0 !important;
font-size: 16px !important;
font-weight: 600 !important;
margin: 16px 0 12px !important;
line-height: 1.4 !important;
}
h6 {
color: #e0e0e0 !important;
font-size: 14px !important;
font-weight: 600 !important;
margin: 16px 0 12px !important;
line-height: 1.4 !important;
}
a {
color: #4f84ff !important;
text-decoration: underline !important;
&:hover {
color: #4f84ff !important;
opacity: 0.8 !important;
}
}
code {
background-color: #2d2d2d !important;
color: #e0e0e0 !important;
border-color: #3d3d3d !important;
}
pre {
background-color: #2d2d2d !important;
border-color: #3d3d3d !important;
color: #e0e0e0 !important;
code {
background-color: transparent !important;
border: none !important;
color: #e0e0e0 !important;
}
}
blockquote {
border-left-color: #4f84ff !important;
background-color: #2d2d2d !important;
color: #b0b0b0 !important;
}
table {
border-color: #3d3d3d !important;
th, td {
border-color: #3d3d3d !important;
background-color: #1a1a1a !important;
color: #e0e0e0 !important;
}
th {
background-color: #2d2d2d !important;
}
}
ul, ol, li {
color: #e0e0e0 !important;
}
hr {
border-top-color: #3d3d3d !important;
}
img, video {
max-width: 100% !important;
border-radius: 4px !important;
}
.w-e-panel-tab-content {
color: #e0e0e0 !important;
}
}
}
}
.el-form-item__label{
min-width: 80px !important;
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M615.3 168.5c2.3 0 4.6 .7 6.6 2.2 1.9 1.4 3.1 3.3 3.8 5 1.4 3.3 1.8 7.3 1.8 11.3 0 2.2-.3 4.9-.7 7.6l0 .4-50.9 264.9 0 0c-4 22.1-8.5 39.4-13.6 51.7-5 12.1-10.9 21.6-18.6 27l0 0c-7.5 5.3-16.9 7.7-27.6 8l-3 0c-16 0-29.3-4.2-36.9-14.8-.1-.1-.1-.1-.2-.2-.1-.2-.3-.4-.5-.6l0 0c-1.8-2.4-2.5-5.2-2.9-7.4-.5-2.8-.7-6.1-.7-9.8 0-7.3 .6-13.5 2.1-18.2 1.4-4.4 4.8-10.4 12.2-10.4l1.3 .1c.2 0 .4 .1 .5 .1 .3 .1 .6 .1 .8 .2 .6 .1 1.3 .3 2.2 .6l7.6 2.2 .1 0 7.6 2.1c.9 .2 1.7 .4 2.2 .5 .4 .1 .6 .1 .7 .1 0 0 .1 0 .2-.1 .1-.1 .4-.2 .8-.6 .9-.9 2.2-2.6 3.6-5.7 2.6-5.8 4.2-14.7 4.2-27.4 0-.7-.3-3.4-1.3-8.9-.9-5.1-2.3-12.2-4.2-21.1l-56-232.9-.1-.4 0-.3c-.4-2.7-.7-5.3-.7-7.3 0-3.7 .4-7.5 1.7-10.6 .7-1.6 1.8-3.4 3.5-4.9 1.9-1.5 4.2-2.3 6.5-2.3l34.6 0c4.6 0 8.7 1.7 11.9 5l1.3 1.5c1.2 1.6 2 3.4 2.7 5.2 .9 2.4 1.5 5.1 2 8.1l30.4 161.2 23.8-160c.8-5.8 2.2-11.4 5.1-15.1 3-3.8 7.2-5.8 12-5.8l24 0zM375.1 34.6c2.6 0 5.1 .7 7.4 2.3 2.2 1.5 3.7 3.6 4.7 5.7 1.9 4 2.5 9.1 2.5 14.5l0 111.2c0 .1 0 .2 0 .3l32.4 0c2.5 0 5 .6 7.2 2.1 2.2 1.4 3.7 3.4 4.8 5.5 2 3.9 2.6 8.8 2.6 13.9l0 15.4c0 5-.6 9.8-2.6 13.7-1.1 2-2.6 4-4.8 5.4-2.2 1.4-4.7 2-7.1 2l-32.5 0 0 145.8c0 10.8 .7 19.5 2 26.2 1.4 6.9 3.1 10.9 4.7 12.9 1.2 1.5 2.2 2.5 3.1 3.2l2.2 1.4 .1 .1 2.9 .8c1.2 .2 2.6 .3 4.3 .3l20.3 0c4.5 0 8.9 1.7 11.8 5.8 2.6 3.6 3.3 8.1 3.3 12.2l0 17.2c0 4.5-.8 9.3-3.4 13.1-3 4.2-7.5 6-12.3 6l0 0-29.1 .1c-8 0-15-.6-21.2-1.7-6.7-1.3-13.4-4.1-20.1-8.1-7.2-4.4-13-10.3-17.5-17.8-4.5-7.5-7.9-17.5-10.5-29.5-2.6-12.2-3.9-26.6-3.9-43l0-144.9-13.4 0c-2.4 0-4.9-.6-7.1-2-2.2-1.4-3.7-3.4-4.8-5.4-2-3.9-2.6-8.7-2.6-13.7l0-15.4c0-5.1 .6-10 2.6-13.9 1.1-2.1 2.6-4 4.8-5.5 2.2-1.5 4.7-2.1 7.2-2.1l13.4 0 0-.6 5.2-111.2 0-.1 .3-3.9c.5-3.8 1.3-7.4 2.7-10.3 2.1-4.2 6-7.9 11.8-7.9l28.6 0zm-268 11.7c1.8 .3 3.6 .9 5.3 2.1 2.1 1.5 3.5 3.5 4.4 5.4 1.7 3.7 2.3 8.4 2.3 13.2l0 377.8c0 6.7-.6 12.6-2.4 17.1-.9 2.3-2.3 4.7-4.4 6.5-2.3 2-5.1 3-8.1 3l-43.7 0c-3 0-5.8-1.1-8.1-3.1-2.1-1.9-3.4-4.3-4.2-6.5-1.7-4.5-2.3-10.3-2.3-17l0-315.8c-4.8 1.2-8.7 2.2-11.5 2.9-1.9 .5-3.4 .8-4.4 1.1-.5 .1-.9 .2-1.2 .3-.1 0-.3 .1-.5 .1-.1 0-.2 0-.3 .1 0 0-.2 0-.3 0 0 0-.2 0-.4 0l0 0c-.1 0-.3 0-.4 0-.1 0-.2 0-.2 0-2.2 .1-4.5-.4-6.6-1.7-2.3-1.4-3.8-3.4-4.7-5.3-1.8-3.7-2.3-8.4-2.3-13l0-27.4c0-4.4 .7-8.8 2.6-12.4 2.1-3.9 5.5-6.6 9.8-7.6l75.5-19.5c1.4-.4 2.9-.5 4.4-.5l1.8 .1zm143.8-.5c1.8 .3 3.6 .9 5.2 2.1 2.1 1.5 3.5 3.5 4.4 5.4 1.7 3.7 2.2 8.4 2.2 13.2l0 377.8c0 6.7-.6 12.6-2.4 17.1-.9 2.3-2.3 4.7-4.4 6.5-2.3 2-5.1 3-8.1 3l-43.7 0c-3 0-5.8-1.1-8.1-3.1-2.1-1.9-3.4-4.3-4.2-6.5-1.7-4.5-2.3-10.3-2.3-17l0-315.8c-4.8 1.2-8.7 2.2-11.5 2.9-1.9 .5-3.4 .8-4.4 1.1-.5 .1-.9 .2-1.2 .3-.1 0-.3 .1-.5 .1-.1 0-.2 0-.3 .1-.1 0-.2 0-.3 0 0 0-.2 0-.4 0l0 0c-.1 0-.2 0-.3 0-.1 0-.2 0-.3 0-2.2 .1-4.5-.4-6.6-1.7-2.3-1.4-3.8-3.4-4.7-5.3-1.8-3.7-2.3-8.3-2.3-13l0-27.4c0-4.4 .7-8.7 2.6-12.4 2.1-3.9 5.5-6.6 9.8-7.6l75.4-19.4c1.4-.4 2.9-.5 4.4-.5l1.8 .1z"/></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M320 96l0 320c21 0 41.8-4.1 61.2-12.2s37-19.8 51.9-34.7 26.6-32.5 34.7-51.9 12.2-40.2 12.2-61.2-4.1-41.8-12.2-61.2-19.8-37.1-34.7-51.9-32.5-26.6-51.9-34.7-40.2-12.2-61.2-12.2zM0 256L160 416 320 256 160 96 0 256zm480 0c0 21 4.1 41.8 12.2 61.2s19.8 37 34.7 51.9 32.5 26.6 51.9 34.7 40.2 12.2 61.2 12.2l0-320c-42.4 0-83.1 16.9-113.1 46.9S480 213.6 480 256z"/></svg>

After

Width:  |  Height:  |  Size: 664 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M71.8 344.3c-6.5-14.2-6.9-18.3 7.4-23.1 25.6-8 8 9.2 43.2 49.2l.3 0 0-93.9c1.2-50.2 44-92.2 97.7-92.2 53.9 0 97.7 43.5 97.7 96.8 0 63.4-60.8 113.2-128.5 93.3-10.5-4.2-2.1-31.7 8.5-28.6 53 0 89.4-10.1 89.4-64.4 0-61-77.1-89.6-116.9-44.6-23.5 26.4-17.6 42.1-17.6 157.6 50.7 31 118.3 22 160.4-20.1 24.8-24.8 38.5-58 38.5-93 0-35.2-13.8-68.2-38.8-93.3-24.8-24.8-57.8-38.5-93.3-38.5S151 163.3 126.3 188c-.3 .3-16 16.5-21.2 23.9l-.5 .6c-3.3 4.7-6.3 9.1-20.1 6.1-6.9-1.7-14.3-5.8-14.3-11.8L70.2 20c0-5 3.9-10.5 10.5-10.5L322 9.5c8.3 0 8.3 11.6 8.3 15.1 0 3.9 0 15.1-8.3 15.1l-223.2 0 0 132.9 .3 0c104.2-109.8 282.8-36 282.8 108.9 0 178.1-244.8 220.3-310.1 62.8zM135.1 83.5c-.5 4.2 4.6 24.5 14.6 20.6 124.8-47.5 202.8 40.4 209.4 40.4 4.8 0 22.8-15.3 14.3-22.8-93.2-89-234.5-57-238.3-38.2zM361.5 414.7c-110 109.9-299 60.8-332-104.2 0-12.2-30.4-7.4-28.9 3.3 24 173.4 246 256.9 381.6 121.3 6.9-7.8-12.6-28.4-20.7-20.4zM182.1 306.6c0 4 4.3 7.3 5.5 8.5 3 3 6.1 4.4 8.5 4.4 3.8 0 2.6 .2 22.3-19.5 19.6 19.3 19.1 19.5 22.3 19.5 5.4 0 18.5-10.4 10.7-18.2l-17.3-17.3 18.2-18.2c6.3-6.8-10.1-21.8-16.2-15.7L218.2 268c-18.6-18.8-18.4-19.5-21.5-19.5-5 0-18 11.7-12.4 17.3L202.5 284c-18.1 17.9-20.4 19.2-20.4 22.6z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M423.9 255.8L411 413.1c-3.3 40.7-63.9 35.1-60.6-4.9l10-122.5-41.1 2.3c10.1 20.7 15.8 43.9 15.8 68.5 0 41.2-16.1 78.7-42.3 106.5l-39.3-39.3c57.9-63.7 13.1-167.2-74-167.2-25.9 0-49.5 9.9-67.2 26L73 243.2c22-20.7 50.1-35.1 81.4-40.2l75.3-85.7-42.6-24.8-51.6 46C105.5 165.3 64.9 120 95 93.1l68-60.7c9.8-8.8 24.1-10.2 35.5-3.6 0 0 139.3 80.9 139.5 81.1 16.2 10.1 20.7 36 6.1 52.6l-58.4 66.5 106.1-5.9c18.5-1.1 33.6 14.4 32.1 32.7zM359 101.8A50.9 50.9 0 1 0 359 0 50.9 50.9 0 1 0 359 101.8zM179.6 456.5c-80.6 0-127.4-90.6-82.7-156.1L57.2 260.7C36.4 287 24 320.3 24 356.4 24 487.1 174.7 557.8 275.4 478.9l-39.7-39.7c-16 10.9-35.3 17.3-56.1 17.3z"/></svg>

After

Width:  |  Height:  |  Size: 949 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M322.1 252l0-1-51.2-65.8s-12 1.6-25 15.1c-9 9.3-242.1 239.1-243.4 240.9-7 10 1.6 6.8 15.7 1.7 .8 0 114.5-36.6 114.5-36.6 .5-.6-.1-.1 .6-.6-.4-5.1-.8-26.2-1-27.7-.6-5.2 2.2-6.9 7-8.9l92.6-33.8c.6-.8 88.5-81.7 90.2-83.3zM482.2 372.1c13.3 16.1 20.7 13.3 30.8 9.3 3.2-1.2 115.4-47.6 117.8-48.9 8-4.3-1.7-16.7-7.2-23.4-2.1-2.5-205.1-245.6-207.2-248.3-9.7-12.2-14.3-12.9-38.4-12.8-10.2 0-106.8 .5-116.5 .6-19.2 .1-32.9-.3-19.2 16.9 7.7 9.5 234.2 299.7 239.9 306.6zm152.7 1.6c-2.3-.3-24.6-4.7-38-7.2 0 0-115 50.4-117.5 51.6-16 7.3-26.9-3.2-36.7-14.6l-57.1-74c-5.4-.9-60.4-9.6-65.3-9.3-3.1 .2-9.6 .8-14.4 2.9-4.9 2.1-145.2 52.8-150.2 54.7-5.1 2-11.4 3.6-11.1 7.6 .2 2.5 2 2.6 4.6 3.5 2.7 .8 300.9 67.6 308 69.1 15.6 3.3 38.5 10.5 53.6 1.7 2.1-1.2 123.8-76.4 125.8-77.8 5.4-4 4.3-6.8-1.7-8.2z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M256 167.5l64.9 98.8-129.8 0 64.9-98.8zM8 256a248 248 0 1 1 496 0 248 248 0 1 1 -496 0zm396.2 82.7l-148.2-223.2-148.2 223.2 30.4 0 33.6-51.7 168.6 0 33.6 51.7 30.2 0z"/></svg>

After

Width:  |  Height:  |  Size: 477 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M482.1 32L28.7 32C5.8 32 0 37.9 0 60.9L0 451.1C0 474.4 5.8 480 28.7 480l453.4 0c24.4 0 29.9-5.2 29.9-29.7l0-388.1c0-24.6-5.4-30.2-29.9-30.2zM178.4 220.3c-27.5-20.2-72.1-8.7-84.2 23.4-4.3 11.1-9.3 9.5-17.5 8.3-9.7-1.5-17.2-3.2-22.5-5.5-28.8-11.4 8.6-55.3 24.9-64.3 41.1-21.4 83.4-22.2 125.3-4.8 40.9 16.8 34.5 59.2 34.5 128.5 2.7 25.8-4.3 58.3 9.3 88.8 1.9 4.4 .4 7.9-2.7 10.7-8.4 6.7-39.3 2.2-46.6-7.4-1.9-2.2-1.8-3.6-3.9-6.2-3.6-3.9-7.3-2.2-11.9 1-57.4 36.4-140.3 21.4-147-43.3-3.1-29.3 12.4-57.1 39.6-71 38.2-19.5 112.2-11.8 114-30.9 1.1-10.2-1.9-20.1-11.3-27.3zm286.7 222c0 15.1-11.1 9.9-17.8 9.9l-394.9 0c-7.4 0-18.2 4.8-17.8-10.7 .4-13.9 10.5-9.1 17.1-9.1 132.3-.4 264.5-.4 396.8 0 6.8 0 16.6-4.4 16.6 9.9zm3.8-340.5l0 291c0 5.7-.7 13.9-8.1 13.9-12.4-.4-27.5 7.1-36.1-5.6-5.8-8.7-7.8-4-12.4-1.2-53.4 29.7-128.1 7.1-144.4-85.2-6.1-33.4-.7-67.1 15.7-100 11.8-23.9 56.9-76.1 136.1-30.5l0-71c0-26.2-.1-26.2 26-26.2 3.1 0 6.6 .4 9.7 0 10.1-.8 13.6 4.4 13.6 14.3-.1 .2-.1 .3-.1 .5zM417.4 334.1c-19.5 47.6-72.9 43.3-90 5.2-15.1-33.3-15.5-68.2 .4-101.5 16.3-34.1 59.7-35.7 81.5-4.8 20.6 28.8 14.9 84.6 8.1 101.1zM122.6 369.4c-7.5-1.3-33-3.3-33.7-27.8-.4-13.9 7.8-23 19.8-25.8 24.4-5.9 49.3-9.9 73.7-14.7 8.9-2 7.4 4.4 7.8 9.5 1.4 33-26.1 59.2-67.6 58.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M159.7 237.4C108.4 308.3 43.1 348.2 14 326.6-15.2 304.9 2.8 230 54.2 159.1 105.5 88.2 170.8 48.3 199.9 69.9s11.1 96.6-40.2 167.5zm351.2-57.3c-73.8 123.4-191.9 187.7-264.5 143.6-25-15.2-41.3-41.2-49-73.8-33.6 64.8-92.8 113.8-164.1 133.2 49.8 59.3 124.1 96.9 207 96.9 150 0 271.6-123.1 271.6-274.9 .1-8.5-.3-16.8-1-25z"/></svg>

After

Width:  |  Height:  |  Size: 627 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M224.5 373.1c-25.2-31.7-40.1-59.4-45-83.2-22.5-88 112.6-88 90.1 0-5.4 24.2-20.3 52-45 83.2l-.1 0zm138.2 73.2c-42.1 18.3-83.7-10.9-119.3-50.5 103.9-130.1 46.1-200-18.8-200-54.9 0-85.2 46.5-73.3 100.5 6.9 29.2 25.2 62.4 54.4 99.5-32.5 36-60.5 52.7-85.2 54.9-50 7.4-89.1-41.1-71.3-91.1 15.1-39.2 111.7-231.2 115.9-241.6 15.8-30.1 25.6-57.4 59.4-57.4 32.3 0 43.4 25.9 60.4 59.9 36 70.6 89.4 177.5 114.8 239.1 13.2 33.1-1.4 71.3-37 86.6zm47-136.1c-128.9-274.3-136-278.2-185.1-278.2-45.5 0-64.9 31.7-84.7 72.8-106.2 212.3-116.4 242.4-117.3 245-25.2 69.3 26.7 130.2 89.6 130.2 21.7 0 60.6-6.1 112.4-62.4 58.7 63.8 101.3 62.4 112.4 62.4 62.9 .1 114.9-60.9 89.6-130.2 0-3.9-16.8-38.9-16.8-39.6l0 0z"/></svg>

After

Width:  |  Height:  |  Size: 1000 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M256 0c-139.9 0-254 112.7-256 252.1-2 141.5 112.9 258.7 254.5 259.5 43.7 .3 85.9-10.4 123.3-30.7 3.6-2 4.2-7 1.1-9.7l-24-21.2c-4.9-4.3-11.8-5.5-17.8-3-26.1 11.1-54.5 16.8-83.7 16.4-114.4-1.5-206.9-96.6-205.1-211 1.8-112.9 94.3-204.2 207.7-204.2l207.7 0 0 369-117.8-104.7c-3.8-3.4-9.7-2.7-12.7 1.3-18.9 25-49.7 40.6-83.9 38.2-47.5-3.3-85.9-41.5-89.5-88.9-4.2-56.6 40.6-103.9 96.3-103.9 50.4 0 91.9 38.8 96.2 88 .4 4.4 2.4 8.5 5.7 11.4l30.7 27.2c3.5 3.1 9 1.2 9.9-3.4 2.2-11.8 3-24.2 2.1-36.8-4.9-72-63.3-130-135.4-134.4-82.7-5.1-151.8 59.5-154 140.6-2.1 78.9 62.6 147 141.6 148.7 33 .7 63.6-9.6 88.3-27.6L495 509.4c6.6 5.8 17 1.2 17-7.7l0-492c0-5.4-4.4-9.7-9.7-9.7L256 0z"/></svg>

After

Width:  |  Height:  |  Size: 981 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M377.7 32L70.3 32C31.4 32 0 63.4 0 102.3L0 409.7C0 448.6 31.4 480 70.3 480l307.5 0c38.5 0 69.8-31.1 70.3-69.6-46-25.6-110.6-60.3-171.6-88.4-32.1 44-84.1 81-148.6 81-70.6 0-93.7-45.3-97-76.4-4-39 14.9-81.5 99.5-81.5 35.4 0 79.4 10.2 127.1 25 16.5-30.1 26.5-60.3 26.5-60.3l-178.2 0 0-16.7 92.1 0 0-31.2-109.4 0 0-19 109.4 0 0-50.4 50.9 0 0 50.4 109.4 0 0 19-109.4 0 0 31.2 88.8 0s-15.2 46.6-38.3 90.9c48.9 16.7 100 36 148.6 52.7l0-234.4c.2-38.7-31.2-70.3-69.9-70.3zM47.3 323c1 20.2 10.2 53.7 69.9 53.7 52.1 0 92.6-39.7 117.9-72.9-44.6-18.7-84.5-31.4-109.4-31.4-67.4 0-79.4 33.1-78.4 50.6z"/></svg>

After

Width:  |  Height:  |  Size: 897 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M14 325.3c2.3-4.2 5.2-4.9 9.7-2.5 10.4 5.6 20.6 11.4 31.2 16.7 40.6 20.2 83.3 35.8 127.4 46.3 20.8 5 41.9 9 63.2 11.8 31.5 4.2 63.2 6 95 5.2 17.4-.4 34.8-1.8 52.1-3.8 56.2-6.6 111.1-21 163.3-42.8 2.9-1.2 5.9-2 9.1-1.2 6.7 1.8 9 9 4.1 13.9-3 2.7-6.2 5.2-9.6 7.4-30.7 21.1-64.2 36.4-99.6 47.9-24.5 7.9-49.6 13.8-75.1 17.6-17.6 2.7-35.4 4.3-53.2 4.8-.8 0-1.7 .2-2.5 .3l-21.1 0c-.8-.1-1.7-.2-2.5-.3-3.6-.2-7.2-.3-10.7-.4-16.9-.8-33.7-2.5-50.4-5.3-27.4-4.5-54.3-11.5-80.4-20.9-53.7-19.4-103.1-48.9-145.6-87-1.8-1.6-3-3.8-4.4-5.7l0-2zM172 65.1l-4.3 .6c-13.8 1.7-26.8 6.9-38 15.1-2.4 1.7-4.6 3.5-7.1 5.4-.2-.4-.3-.9-.4-1.4-.4-2.7-.8-5.5-1.3-8.2-.7-4.6-3-6.6-7.6-6.6l-11.5 0c-6.9 0-8.2 1.3-8.2 8.2l0 209.3c0 1 0 2 .1 3 .2 3 2 4.9 4.9 5 7 .1 14.1 .1 21.1 0 2.9 0 4.7-2 5-5 .1-1 .1-2 .1-3l0-72.4c1.1 .9 1.7 1.4 2.2 1.9 17.9 14.9 38.5 19.8 61 15.4 20.4-4 34.6-16.5 43.8-34.9 7-13.9 9.9-28.7 10.3-44.1 .5-17.1-1.2-33.9-8.1-49.8-8.5-19.6-22.6-32.5-43.9-36.9-3.2-.7-6.5-1-9.8-1.5-2.8-.1-5.5-.1-8.3-.1zM124.6 107c-.1-.7 .1-1.3 .4-1.9s.8-1.1 1.3-1.4c13.7-9.5 28.8-14.5 45.6-13.2 14.9 1.1 27.1 8.4 33.5 25.9 3.9 10.7 4.9 21.8 4.9 33 0 10.4-.8 20.6-4 30.6-6.8 21.3-22.4 29.4-42.6 28.5-14-.6-26.2-6-37.4-13.9-.6-.3-1-.8-1.3-1.4s-.4-1.2-.4-1.9c.1-14.1 0-28.1 0-42.2s.1-28 0-42.1zM330.3 65.1c-1 .1-2 .3-2.9 .4-9.7 .4-19.4 1.8-28.9 4.1-6.1 1.6-12 3.8-17.9 5.8-3.6 1.2-5.4 3.8-5.3 7.7 .1 3.3-.1 6.6 0 9.9 .1 4.8 2.1 6.1 6.8 4.9 7.8-2 15.6-4.2 23.5-5.7 12.3-2.3 24.7-3.3 37.2-1.4 6.5 1 12.6 2.9 16.8 8.4 3.7 4.8 5.1 10.5 5.3 16.4 .3 8.3 .2 16.6 .3 24.9 0 .5-.1 .9-.2 1.4-.5-.1-.9 0-1.3-.1-10.5-2.6-21.2-4.2-32-4.9-11.3-.6-22.5 .1-33.3 3.9-12.9 4.5-23.3 12.3-29.4 24.9-4.7 9.8-5.4 20.2-3.9 30.7 2 14 9 24.8 21.4 31.7 11.9 6.6 24.8 7.4 37.9 5.4 15.1-2.3 28.5-8.7 40.3-18.4 .5-.4 1-.8 1.6-1.1 .6 3.8 1.1 7.4 1.8 11 .6 3.1 2.5 5.1 5.4 5.2 5.4 .1 10.9 .1 16.3 0 1.3 0 2.5-.5 3.4-1.4s1.4-2.1 1.5-3.3c.1-.9 .1-1.9 .1-2.8l0-106c0-4.3-.3-8.6-.9-12.9-1.9-12.9-7.4-23.5-19-30.4-6.7-4-14.1-6-21.8-7.1-3.6-.5-7.2-.8-10.8-1.3-3.9 .1-7.9 .1-11.9 .1zm35 127.7c0 .6-.1 1.2-.3 1.7s-.7 1-1.2 1.3c-11.2 8.1-23.5 13.5-37.4 14.9-5.7 .6-11.4 .4-16.8-1.8-3-1.1-5.6-2.9-7.8-5.2s-3.7-5.1-4.6-8.1c-2-6.3-2-13.1-.1-19.4 2.5-8.3 8.4-13 16.4-15.6 8-2.4 16.5-3.2 24.8-2.2 8.4 .7 16.6 2.3 25 3.4 1.6 .2 2.1 1 2.1 2.6-.1 4.8 0 9.5 0 14.3s-.2 9.4-.1 14.1zM625.2 322.2c-1-5-4.8-6.9-9.1-8.3-6.8-2.1-13.9-3.5-21-3.9-13.1-1.1-26.3-.5-39.2 1.9-14.3 2.7-27.9 7.3-40 15.6-1.4 .9-2.7 2.1-3.7 3.5-.4 .6-.6 1.3-.7 1.9s0 1.4 .2 2.1c.4 1.5 2.1 1.9 3.6 1.8 .7 0 1.5 0 2.2-.1 7.8-.8 15.5-1.7 23.3-2.5 11.4-1.1 22.9-1.8 34.3-.9 4.9 .4 9.7 1.3 14.4 2.7 5.1 1.4 7.4 5.2 7.6 10.4 .4 8-1.4 15.7-3.5 23.3-4.1 15.4-10 30.3-15.8 45.1-.4 1-.8 2-1 3-.5 2.9 1.2 4.8 4.1 4.1 1.8-.4 3.4-1.3 4.8-2.5 4.5-4.2 8.8-8.7 12.7-13.4 12.8-16.4 20.3-35.3 24.7-55.6 .8-3.6 1.4-7.3 2.1-10.9l0-17.3zM493.1 199c-12.9-35.7-25.8-71.4-38.7-107.2-2-5.7-4.2-11.3-6.3-16.9-1.1-2.9-3.2-4.8-6.4-4.8-7.6-.1-15.2-.2-22.9-.1-2.5 0-3.7 2-3.2 4.5 .5 2.1 1.1 4.1 1.9 6.1 19.6 48.5 39.3 97 59.1 145.5 1.7 4.1 2.1 7.6 .2 11.8-3.3 7.3-5.9 15-9.3 22.3-3 6.5-8 11.4-15.2 13.3-5 1.3-10.2 1.7-15.4 1.1-2.5-.2-5-.8-7.5-1-3.4-.2-5.1 1.3-5.2 4.8-.1 3.3-.1 6.6 0 9.9 .1 5.5 2 8 7.4 8.9 5.6 1.1 11.2 1.8 16.9 2 17.1 .4 30.7-6.5 39.5-21.4 3.5-5.9 6.6-12 9.2-18.4 23.7-59.8 47.2-119.7 70.6-179.6 .7-1.8 1.3-3.6 1.6-5.5 .4-2.8-.9-4.4-3.7-4.4-6.6-.1-13.3 0-19.9 0-1.7-.1-3.4 .3-4.7 1.3s-2.4 2.3-3 3.9c-.5 1.4-1.1 2.7-1.6 4.1l-34.8 100c-2.5 7.2-5.1 14.5-7.7 22.2-.4-1.1-.6-1.7-.9-2.4z"/></svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M257.7 162.7c-48.7 1.8-169.5 15.5-169.5 117.5 0 109.5 138.3 114 183.5 43.2 6.5 10.2 35.4 37.5 45.3 46.8l56.8-56s-32.3-25.3-32.3-52.8l0-147.1C341.5 89 317 32 229.2 32 141.2 32 94.5 87 94.5 136.3l73.5 6.8c16.3-49.5 54.2-49.5 54.2-49.5 40.7-.1 35.5 29.8 35.5 69.1zm0 86.8c0 80-84.2 68-84.2 17.2 0-47.2 50.5-56.7 84.2-57.8l0 40.6zM393.7 413c-7.7 10-70 67-174.5 67S34.7 408.5 10.2 379c-6.8-7.7 1-11.3 5.5-8.3 73.3 44.5 187.8 117.8 372.5 30.3 7.5-3.7 13.3 2 5.5 12zm39.8 2.2c-6.5 15.8-16 26.8-21.2 31-5.5 4.5-9.5 2.7-6.5-3.8s19.3-46.5 12.7-55c-6.5-8.3-37-4.3-48-3.2-10.8 1-13 2-14-.3-2.3-5.7 21.7-15.5 37.5-17.5 15.7-1.8 41-.8 46 5.7 3.7 5.1 0 27.1-6.5 43.1z"/></svg>

After

Width:  |  Height:  |  Size: 963 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M240.2 32c-61.9 0-131.5 16.9-184.2 55.4-5.1 3.1-9.1 9.2-7.2 19.4 1.1 5.1 5.1 27.4 10.2 39.6 4.1 10.2 14.2 10.2 20.3 6.1 32.5-22.3 96.5-47.7 152.3-47.7 57.9 0 58.9 28.4 58.9 73.1l0 38.5C203.1 227.7 78.3 251 46.8 264.2 11.3 280.5 16.4 357.7 16.4 376S31.6 480 141.3 480c47.8 0 113.7-20.7 153.3-42.1l0 25.4c0 3 2.1 8.2 6.1 9.1 3.1 1 50.7 2 59.9 2s62.5 .3 66.5-.7c4.1-1 5.1-6.1 5.1-9.1l0-296.6c-.1-80.3-57.9-136-192-136zm50.2 348c-21.4 13.2-48.7 24.4-79.1 24.4-52.8 0-58.9-33.5-59-44.7 0-12.2-3-42.7 18.3-52.9 24.3-13.2 75.1-29.4 119.8-33.5l0 106.7z"/></svg>

After

Width:  |  Height:  |  Size: 855 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M420.5 253.9a24 24 0 1 1 0 48 24 24 0 1 1 0-48zm-265.1 0a24 24 0 1 1 0 48 24 24 0 1 1 0-48zm273.7-96.5l47.9-83c.8-1.1 1.3-2.4 1.5-3.8s.2-2.7-.1-4.1-.9-2.6-1.7-3.7-1.8-2-3-2.7-2.5-1.1-3.9-1.3-2.7 0-4 .4-2.5 1.1-3.6 1.9-1.9 2-2.5 3.2l-48.5 84.1c-38.8-17.4-80.8-26.4-123.3-26.4s-84.5 9-123.3 26.4L116.2 64.4c-.6-1.2-1.5-2.3-2.5-3.2s-2.3-1.5-3.6-1.9-2.7-.5-4-.4-2.7 .6-3.9 1.3-2.2 1.6-3 2.7-1.4 2.4-1.7 3.7-.3 2.7-.1 4.1 .8 2.6 1.5 3.8l47.9 83C64.5 202.2 8.2 285.5 0 384l576 0c-8.2-98.5-64.5-181.8-146.9-226.6z"/></svg>

After

Width:  |  Height:  |  Size: 817 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M315.6 215.4c11.7-32.6 45.4-126.9 45.4-157.1 0-26.6-15.7-48.9-43.7-48.9-44.6 0-84.6 131.7-97.1 163.1-9.7-28.5-55.1-172.5-95.1-172.5-31.1 0-45.7 22.9-45.7 51.7 0 35.3 34.2 126.8 46.6 162-6.3-2.3-13.1-4.3-20-4.3-23.4 0-48.3 29.1-48.3 52.6 0 8.9 4.9 21.4 8 29.7-36.9 10-51.1 34.6-51.1 71.7-.1 72.2 68.3 148.6 164.5 148.6 118 0 191.4-88.6 191.4-202.9 0-43.1-6.9-82-54.9-93.7zM280.2 108c4-12.3 21.1-64.3 37.1-64.3 8.6 0 10.9 8.9 10.9 16 0 19.1-38.6 124.6-47.1 148l-34-6 33.1-93.7zM110.8 48.3c0-11.9 14.5-45.7 46.3 47.1l34.6 100.3c-15.6-1.3-27.7-3-35.4 1.4-10.9-28.8-45.5-119.7-45.5-148.8zM108.5 244c29.3 0 67.1 94.6 67.1 107.4 0 5.1-4.9 11.4-10.6 11.4-20.9 0-76.9-76.9-76.9-97.7 .1-7.7 12.7-21.1 20.4-21.1zM292.8 430.3c-29.1 32-66.3 48.6-109.7 48.6-59.4 0-106.3-32.6-128.9-88.3-17.1-43.4 3.8-68.3 20.6-68.3 11.4 0 54.3 60.3 54.3 73.1 0 4.9-7.7 8.3-11.7 8.3-16.1 0-22.4-15.5-51.1-51.4-29.7 29.7 20.5 86.9 58.3 86.9 26.1 0 43.1-24.2 38-42 3.7 0 8.3 .3 11.7-.6 1.1 27.1 9.1 59.4 41.7 61.7 0-.9 2-7.1 2-7.4 0-17.4-10.6-32.6-10.6-50.3 0-28.3 21.7-55.7 43.7-71.7 8-6 17.7-9.7 27.1-13.1 9.7-3.7 20-8 27.4-15.4-1.1-11.2-5.7-21.1-16.9-21.1-27.7 0-120.6 4-120.6-39.7 0-6.7 .1-13.1 17.4-13.1 32.3 0 114.3 8 138.3 29.1 18.1 16.1 24.3 113.2-31 174.7zm-98.6-126c9.7 3.1 19.7 4 29.7 6-7.4 5.4-14 12-20.3 19.1-2.8-8.5-6.2-16.8-9.4-25.1z"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M640 238.2l-3.2 28.2-34.5 2.3-2 18.1 34.5-2.3-3.2 28.2-34.4 2.2-2.3 20.1 34.4-2.2-3 26.1-64.7 4.1 12.7-113.2-47.3 115.4-31.9 2-23.8-117.8 30.3-2 13.6 79.4 31.7-82.4 93.1-6.2zM426.8 371.5l28.3-1.8 12.9-120.1-28.4 1.9-12.8 120zM162 388.1l-19.4-36-3.5 37.4-28.2 1.7 2.7-29.1c-11 18-32 34.3-56.9 35.8-32.8 2-59.7-20.9-56.4-58.2 2.6-29.3 26.7-62.8 67.5-65.4 37.7-2.4 47.6 23.2 51.3 28.8l2.8-30.8 38.9-2.5c20.1-1.3 38.7 3.7 42.5 23.7l2.6-26.6 64.8-4.2-2.7 27.9-36.4 2.4-1.7 17.9 36.4-2.3-2.7 27.9-36.4 2.3-1.9 19.9 36.3-2.3-2.1 20.8 55-117.2 23.8-1.6 32.1 110.6 8.9-85.6-22.3 1.4 2.9-27.9 75-4.9-3 28-24.3 1.6-9.7 91.9-58 3.7-4.3-15.6-39.4 2.5-8 16.3-126.2 7.7zm-44.3-70.2l-26.4 1.7c-6.7-12.4-14.4-16.6-26.3-15.8-19 1.2-33.3 17.5-34.6 33.3-1.4 16 7.3 32.5 28.7 31.2 12.8-.8 21.3-8.6 28.9-18.9l27-1.7 2.7-29.8zm56.1-7.7c1.2-12.9-7.6-13.6-26.1-12.4L145 326.3c14.2-.9 27.5-2.1 28.8-16.1zM194.9 381l5.8-60c-5 13.5-14.7 21.1-27.9 26.6L194.9 381zm135.4-45l-7.9-37.8-15.8 39.3 23.7-1.5zM160.2 261.4l-4.3-17.5-39.6 2.6-8.1 18.2-31.9 2.1 57-121.9 23.9-1.6 30.7 102 9.9-104.7 27-1.8 37.8 63.6 6.5-66.6 28.5-1.9-4 41.2c7.4-13.5 22.9-44.7 63.6-47.5 40.5-2.8 52.4 29.3 53.4 30.3l3.3-32 39.3-2.7c12.7-.9 27.8 .3 36.3 9.7l-4.4-11.9 32.2-2.2 12.9 43.2 23-45.7 31-2.2-43.6 78.4-4.8 44.3-28.4 1.9 4.8-44.3-15.8-43c1 22.3-9.2 40.1-32 49.6l25.2 38.8-36.4 2.4-19.2-36.8-4 38.3-28.4 1.9 3.3-31.5c-6.7 9.3-19.7 35.4-59.6 38-26.2 1.7-45.6-10.3-55.4-39.2l-4 40.3-25 1.6-37.6-63.3-6.3 66.2-56.8 3.7zm276.6-82.1c10.2-.7 17.5-2.1 21.6-4.3 4.5-2.4 7-6.4 7.6-12.1 .6-5.3-.6-8.8-3.4-10.4-3.6-2.1-10.6-2.8-22.9-2l-2.9 28.8zM327.7 214c5.6 5.9 12.7 8.5 21.3 7.9 4.7-.3 9.1-1.8 13.3-4.1 5.5-3 10.6-8 15.1-14.3l-34.2 2.3 2.4-23.9 63.1-4.3 1.2-12-31.2 2.1c-4.1-3.7-7.8-6.6-11.1-8.1-4-1.7-8.1-2.8-12.2-2.5-8 .5-15.3 3.6-22 9.2-7.7 6.4-12 14.5-12.9 24.4-1.1 9.6 1.4 17.3 7.2 23.3zm-201.3 8.2l23.8-1.6-8.3-37.6-15.5 39.2z"/></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M185.7 268.1l76.2 0-38.1-91.6-38.1 91.6zM223.8 32L16 106.4 47.8 382.1 223.8 480 399.8 382.1 431.6 106.4 223.8 32zM354 373.8l-48.6 0-26.2-65.4-110.6 0-26.2 65.4-48.7 0 130.1-292.3 130.2 292.3z"/></svg>

After

Width:  |  Height:  |  Size: 502 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M400 32L48 32C21.5 32 0 53.5 0 80L0 432c0 26.5 21.5 48 48 48l352 0c26.5 0 48-21.5 48-48l0-352c0-26.5-21.5-48-48-48zM127 384.5c-5.5 9.6-17.8 12.8-27.3 7.3-9.6-5.5-12.8-17.8-7.3-27.3l14.3-24.7c16.1-4.9 29.3-1.1 39.6 11.4L127 384.5zm138.9-53.9L84 330.6c-11 0-20-9-20-20s9-20 20-20l51 0 65.4-113.2-20.5-35.4c-5.5-9.6-2.2-21.8 7.3-27.3 9.6-5.5 21.8-2.2 27.3 7.3l8.9 15.4 8.9-15.4c5.5-9.6 17.8-12.8 27.3-7.3 9.6 5.5 12.8 17.8 7.3 27.3l-85.8 148.6 62.1 0c20.2 0 31.5 23.7 22.7 40zm98.1 0l-29 0 19.6 33.9c5.5 9.6 2.2 21.8-7.3 27.3-9.6 5.5-21.8 2.2-27.3-7.3-32.9-56.9-57.5-99.7-74-128.1-16.7-29-4.8-58 7.1-67.8 13.1 22.7 32.7 56.7 58.9 102l52 0c11 0 20 9 20 20 0 11.1-9 20-20 20z"/></svg>

After

Width:  |  Height:  |  Size: 981 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M255.9 120.9l9.1-15.7c5.6-9.8 18.1-13.1 27.9-7.5s13.1 18.1 7.5 27.9l-87.5 151.5 63.3 0c20.5 0 32 24.1 23.1 40.8l-185.5 0c-11.3 0-20.4-9.1-20.4-20.4s9.1-20.4 20.4-20.4l52 0 66.6-115.4-20.8-36.1c-5.6-9.8-2.3-22.2 7.5-27.9 9.8-5.6 22.2-2.3 27.9 7.5l8.9 15.7zm-78.7 218l-19.6 34c-5.6 9.8-18.1 13.1-27.9 7.5s-13.1-18.1-7.5-27.9l14.6-25.2c16.4-5.1 29.8-1.2 40.4 11.6zm168.9-61.7l53.1 0c11.3 0 20.4 9.1 20.4 20.4S410.5 318 399.2 318l-29.5 0 19.9 34.5c5.6 9.8 2.3 22.2-7.5 27.9-9.8 5.6-22.2 2.3-27.9-7.5-33.5-58.1-58.7-101.6-75.4-130.6-17.1-29.5-4.9-59.1 7.2-69.1 13.4 23 33.4 57.7 60.1 104zM256 8a248 248 0 1 0 0 496 248 248 0 1 0 0-496zM40 256a216 216 0 1 1 432 0 216 216 0 1 1 -432 0z"/></svg>

After

Width:  |  Height:  |  Size: 990 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M42.1 239.1c22.2 0 29 2.8 33.5 14.6l.8 0 0-22.9c0-11.3-4.8-15.4-17.9-15.4-11.3 0-14.4 2.5-15.1 12.8l-38.6 0c.3-13.9 1.5-19.1 5.8-24.4 7.3-8.8 18.9-11.8 46.1-11.8 33 0 47.1 5 53.9 18.9 2 4.3 4 15.6 4 23.7l0 76.3-38.3 0 1.3-19.1-1 0c-5.3 15.6-13.6 20.4-35.5 20.4-30.3 0-41.1-10.1-41.1-37.3 0-25.2 12.3-35.8 42.1-35.8zm17.1 48.1c13.1 0 16.9-3 16.9-13.4 0-9.1-4.3-11.6-19.6-11.6-13.1 0-17.9 3-17.9 12.1-.1 10.4 3.7 12.9 20.6 12.9zM137 192.3l38.3 0-1.5 20.6 .8 0c9.1-17.1 15.9-20.9 37.5-20.9 14.4 0 24.7 3 31.5 9.1 9.8 8.6 12.8 20.4 12.8 48.1 0 30-3 43.1-12.1 52.9-6.8 7.3-16.4 10.1-33.2 10.1-20.4 0-29.2-5.5-33.8-21.2l-.8 0 0 70.3-39.5 0 0-169zM217.9 253c0-27.5-3.3-32.5-20.7-32.5-16.9 0-20.7 5-20.7 28.7 0 28 3.5 33.5 21.2 33.5 16.4 0 20.2-5.6 20.2-29.7zm57.9-60.7l38.3 0-1.5 20.6 .8 0c9.1-17.1 15.9-20.9 37.5-20.9 14.4 0 24.7 3 31.5 9.1 9.8 8.6 12.8 20.4 12.8 48.1 0 30-3 43.1-12.1 52.9-6.8 7.3-16.4 10.1-33.3 10.1-20.4 0-29.2-5.5-33.8-21.2l-.8 0 0 70.3-39.5 0 0-169 .1 0zM356.7 253c0-27.5-3.3-32.5-20.7-32.5-16.9 0-20.7 5-20.7 28.7 0 28 3.5 33.5 21.2 33.5 16.4 0 20.2-5.6 20.2-29.7zm53.8-3.8c0-25.4 3.3-37.8 12.3-45.8 8.8-8.1 22.2-11.3 45.1-11.3 42.8 0 55.7 12.8 55.7 55.7l0 11.1-75.3 0c-.3 2-.3 4-.3 4.8 0 16.9 4.5 21.9 20.1 21.9 13.9 0 17.9-3 17.9-13.9l37.5 0 0 2.3c0 9.8-2.5 18.9-6.8 24.7-7.3 9.8-19.6 13.6-44.3 13.6-27.5 0-41.6-3.3-50.6-12.3-8.5-8.5-11.3-21.3-11.3-50.8zm76.4-11.6c-.3-1.8-.3-3.3-.3-3.8 0-12.3-3.3-14.6-19.6-14.6-14.4 0-17.1 3-18.1 15.1l-.3 3.3 38.3 0zm55.6-45.3l38.3 0-1.8 19.9 .7 0c6.8-14.9 14.4-20.2 29.7-20.2 10.8 0 19.1 3.3 23.4 9.3 5.3 7.3 6.8 14.4 6.8 34 0 1.5 0 5 .2 9.3l-35 0c.3-1.8 .3-3.3 .3-4 0-15.4-2-19.4-10.3-19.4-6.3 0-10.8 3.3-13.1 9.3-1 3-1 4.3-1 12.3l0 68-38.3 0 0-118.5 .1 0z"/></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M116.9 158.5c-7.5 8.9-19.5 15.9-31.5 14.9-1.5-12 4.4-24.8 11.3-32.6 7.5-9.1 20.6-15.6 31.3-16.1 1.2 12.4-3.7 24.7-11.1 33.8zm10.9 17.2c-17.4-1-32.3 9.9-40.5 9.9-8.4 0-21-9.4-34.8-9.1-17.9 .3-34.5 10.4-43.6 26.5-18.8 32.3-4.9 80 13.3 106.3 8.9 13 19.5 27.3 33.5 26.8 13.3-.5 18.5-8.6 34.5-8.6 16.1 0 20.8 8.6 34.8 8.4 14.5-.3 23.6-13 32.5-26 10.1-14.8 14.3-29.1 14.5-29.9-.3-.3-28-10.9-28.3-42.9-.3-26.8 21.9-39.5 22.9-40.3-12.5-18.6-32-20.6-38.8-21.1zm100.4-36.2l0 194.9 30.3 0 0-66.6 41.9 0c38.3 0 65.1-26.3 65.1-64.3s-26.4-64-64.1-64l-73.2 0zM258.5 165l34.9 0c26.3 0 41.3 14 41.3 38.6s-15 38.8-41.4 38.8l-34.8 0 0-77.4zM420.7 335.9c19 0 36.6-9.6 44.6-24.9l.6 0 0 23.4 28 0 0-97c0-28.1-22.5-46.3-57.1-46.3-32.1 0-55.9 18.4-56.8 43.6l27.3 0c2.3-12 13.4-19.9 28.6-19.9 18.5 0 28.9 8.6 28.9 24.5l0 10.8-37.8 2.3c-35.1 2.1-54.1 16.5-54.1 41.5 .1 25.2 19.7 42 47.8 42zm8.2-23.1c-16.1 0-26.4-7.8-26.4-19.6 0-12.3 9.9-19.4 28.8-20.5l33.6-2.1 0 11c0 18.2-15.5 31.2-36 31.2zm102.5 74.6c29.5 0 43.4-11.3 55.5-45.4l53.1-149-30.8 0-35.6 115.1-.6 0-35.6-115.1-31.6 0 51.2 141.9-2.8 8.6c-4.6 14.6-12.1 20.3-25.5 20.3-2.4 0-7-.3-8.9-.5l0 23.4c1.8 .4 9.3 .7 11.6 .7z"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M319.1 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7-55.8 .9-115.1 44.5-115.1 133.2 0 26.2 4.8 53.3 14.4 81.2 12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zM262.5 104.5c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z"/></svg>

After

Width:  |  Height:  |  Size: 759 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M2.1 377.4l43 74.3c4.3 8.5 10.8 15.7 18.9 20.7s17.4 7.6 27 7.6l285.4 0-59.2-102.6-315.1 0zM501.9 350L335.7 59.3c-4.4-8.2-10.9-15.1-18.9-20S299.7 32 290.4 32l-88.4 0 257.3 447.6 40.7-70.5c1.9-3.2 21-29.7 2-59.1zM275.2 304.5l-115.5-200-115.5 200 231 0z"/></svg>

After

Width:  |  Height:  |  Size: 561 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M517.5 309.2c38.8-40 58.1-80 58.5-116.1 .8-65.5-59.4-118.2-169.4-135-128.7-19.7-288.5 15.5-406.6 82.4 52-26.5 110.6-48.2 170.7-58.2 74.5-20.5 153-25.4 221.3-14.8 152.5 23.8 196.8 127.5 98.8 231.7-10.2 10.8-22 21.1-35 30.6L304.9 103.4 114.7 388.9c-65.6-29.4-76.5-90.2-19.1-151.2 20.8-22.2 48.3-41.9 79.5-58.1 20-12.2 39.7-22.6 62-30.7-65.1 20.3-122.7 52.9-161.6 92.9-27.7 28.6-41.4 57.1-41.7 82.9-.5 35.1 23.4 65.1 68.4 83l-34.5 51.7 101.6 0 22-34.4c22.2 1 45.3 0 68.6-2.7l-22.8 37.1 135.5 0-32.6-53.1c18.6-5.3 36.9-11.5 54.5-18.7l45.9 71.8 101.6 0-73.4-110.4c18.5-12.1 35-25.5 48.9-39.8zM329.9 389.7l-25-40.6-32.7 53.3c-23.4 3.5-46.7 5.1-69.2 4.4l101.9-159.3 78.7 123c-17.2 7.4-35.3 13.9-53.7 19.2z"/></svg>

After

Width:  |  Height:  |  Size: 1009 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M152.2 236.4c-7.7-8.2-19.7-7.7-24.8 2.8L1.6 490.2c-5 10 2.4 21.7 13.4 21.7l175 0c5.8 .1 11-3.2 13.4-8.4 37.9-77.8 15.1-196.3-51.2-267.1zM244.4 8.1c-122.3 193.4-8.5 348.6 65 495.5 2.5 5.1 7.7 8.4 13.4 8.4L497 512c11.2 0 18.4-11.8 13.4-21.7 0 0-234.5-470.6-240.4-482.3-5.3-10.6-18.8-10.8-25.6 .1z"/></svg>

After

Width:  |  Height:  |  Size: 605 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M640 199.9l0 54-320 200-320-199.9 0-54 320 200 320-200.1zm-194.5 72l47.1-29.4c-37.2-55.8-100.7-92.6-172.7-92.6s-135.5 36.7-172.6 92.4l.3 0c2.5-2.3 5.1-4.5 7.7-6.7 89.7-74.4 219.4-58.1 290.2 36.3zM225.4 290.7c16.9-11.9 36.5-18.7 57.4-18.7 34.4 0 65.2 18.4 86.4 47.6l45.4-28.4c-20.9-29.9-55.6-49.5-94.8-49.5-38.9 0-73.4 19.4-94.4 49zM103.6 161.1C235.4 56.8 421.8 84.7 521.1 223.2l.7 1 48.8-30.4c-53.5-81.7-145.8-135.7-250.7-135.7-103.5 0-196.6 53.5-250.5 135.6 9.9-10.5 22.7-23.5 34.2-32.6z"/></svg>

After

Width:  |  Height:  |  Size: 799 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M318.4 16l-161 480 77.5 0 25.4-81.4 119.5 0 25.2 81.4 77.5 0-164.1-480zM278.1 357.9l41.2-130.4 1.5 0 40.9 130.4-83.6 0zM640 405L630 373.6 462.1 358 481.5 414.5 640 405zM177.9 358L10 373.7 0 405 158.5 414.4 177.9 358z"/></svg>

After

Width:  |  Height:  |  Size: 527 B

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