增加七牛云存储
This commit is contained in:
parent
0aa25cead2
commit
97c0142425
73
docs/README.md
Normal file
73
docs/README.md
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# Platform前端项目文档
|
||||||
|
|
||||||
|
## 📚 文档目录
|
||||||
|
|
||||||
|
### 功能使用文档
|
||||||
|
- [字典使用说明](./dictionary-usage.md)
|
||||||
|
- [Pinia字典指南](./pinia-dict-guide.md)
|
||||||
|
- [调用字典](./调用字典.md)
|
||||||
|
- [接口调用](./接口调用.md)
|
||||||
|
- [拼接接口路径](./拼接接口路径.md)
|
||||||
|
- [获取缓存数据](./获取缓存数据.md)
|
||||||
|
- [调用图片上传组件](./调用图片上传组件.md)
|
||||||
|
- [一键复制](./一键复制.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/)
|
||||||
|
|
||||||
|
## 📝 更新日志
|
||||||
|
|
||||||
|
### 2024-01-01
|
||||||
|
- ✅ 完成存储配置界面
|
||||||
|
- ✅ 支持本地存储和七牛云存储配置
|
||||||
|
- ✅ 表单验证和草稿保存
|
||||||
|
- ✅ 完善文档体系
|
||||||
101
docs/图片URL处理说明.md
Normal file
101
docs/图片URL处理说明.md
Normal 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
|
||||||
@ -127,3 +127,27 @@ export function saveCompanySeo(data) {
|
|||||||
data: data,
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
42
src/utils/url.js
Normal file
42
src/utils/url.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* URL工具函数
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取完整的文件URL
|
||||||
|
* 如果URL已经是完整URL(http://或https://开头),直接返回
|
||||||
|
* 否则拼接API基础URL
|
||||||
|
* @param {string} url - 文件URL或路径
|
||||||
|
* @returns {string} 完整的URL
|
||||||
|
*/
|
||||||
|
export function getFileUrl(url) {
|
||||||
|
if (!url) return '';
|
||||||
|
|
||||||
|
// 如果URL已经是完整的URL(以http://或https://开头),直接返回
|
||||||
|
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}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取环境URL(getEnvUrl的别名)
|
||||||
|
* @param {string} path - 文件路径
|
||||||
|
* @returns {string} 完整的URL
|
||||||
|
*/
|
||||||
|
export function getEnvUrl(path) {
|
||||||
|
return getFileUrl(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断URL是否是完整URL
|
||||||
|
* @param {string} url - URL字符串
|
||||||
|
* @returns {boolean} 是否是完整URL
|
||||||
|
*/
|
||||||
|
export function isFullUrl(url) {
|
||||||
|
if (!url) return false;
|
||||||
|
return url.startsWith('http://') || url.startsWith('https://');
|
||||||
|
}
|
||||||
@ -3,15 +3,34 @@
|
|||||||
<div class="login-card">
|
<div class="login-card">
|
||||||
<div class="login-side">
|
<div class="login-side">
|
||||||
<div class="brand">
|
<div class="brand">
|
||||||
<img src="@/assets/svgs/logo-w.svg" alt="Logo" style="color: white;width: 50px;" />
|
<img
|
||||||
|
src="@/assets/svgs/logo-w.svg"
|
||||||
|
alt="Logo"
|
||||||
|
style="color: white; width: 50px"
|
||||||
|
/>
|
||||||
<span class="brand-title">后台管理系统</span>
|
<span class="brand-title">后台管理系统</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="illus">
|
<div class="illus">
|
||||||
<svg viewBox="0 0 300 160" style="max-width: 100%" fill="none">
|
<svg viewBox="0 0 300 160" style="max-width: 100%" fill="none">
|
||||||
<ellipse cx="150" cy="140" rx="120" ry="16" fill="#edf4fd" />
|
<ellipse cx="150" cy="140" rx="120" ry="16" fill="#edf4fd" />
|
||||||
<rect x="57" y="58" width="60" height="40" rx="12" fill="#64b6f7" />
|
<rect x="57" y="58" width="60" height="40" rx="12" fill="#64b6f7" />
|
||||||
<rect x="125" y="46" width="110" height="64" rx="14" fill="#389bf7" opacity="0.11" />
|
<rect
|
||||||
<rect x="136" y="60" width="60" height="41" rx="10" fill="#b8e1ff" />
|
x="125"
|
||||||
|
y="46"
|
||||||
|
width="110"
|
||||||
|
height="64"
|
||||||
|
rx="14"
|
||||||
|
fill="#389bf7"
|
||||||
|
opacity="0.11"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="136"
|
||||||
|
y="60"
|
||||||
|
width="60"
|
||||||
|
height="41"
|
||||||
|
rx="10"
|
||||||
|
fill="#b8e1ff"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<!-- 版权信息 -->
|
<!-- 版权信息 -->
|
||||||
@ -24,36 +43,96 @@
|
|||||||
<span class="input-icon">
|
<span class="input-icon">
|
||||||
<i class="fa-solid fa-user"></i>
|
<i class="fa-solid fa-user"></i>
|
||||||
</span>
|
</span>
|
||||||
<input v-model="account" type="text" placeholder="用户名" autocomplete="account" class="input input-with-icon" />
|
<input
|
||||||
|
v-model="account"
|
||||||
|
type="text"
|
||||||
|
placeholder="用户名"
|
||||||
|
autocomplete="account"
|
||||||
|
class="input input-with-icon"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group icon-input-group">
|
<div class="form-group icon-input-group">
|
||||||
<span class="input-icon">
|
<span class="input-icon">
|
||||||
<i class="fa-solid fa-lock"></i>
|
<i class="fa-solid fa-lock"></i>
|
||||||
</span>
|
</span>
|
||||||
<input v-model="password" :type="passwordVisible ? 'text' : 'password'" placeholder="密码"
|
<input
|
||||||
autocomplete="current-password" class="input input-with-icon" />
|
v-model="password"
|
||||||
<span class="visible-btn" @click="passwordVisible = !passwordVisible"
|
:type="passwordVisible ? 'text' : 'password'"
|
||||||
:title="passwordVisible ? '隐藏密码' : '显示密码'">
|
placeholder="密码"
|
||||||
|
autocomplete="current-password"
|
||||||
|
class="input input-with-icon"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="visible-btn"
|
||||||
|
@click="passwordVisible = !passwordVisible"
|
||||||
|
:title="passwordVisible ? '隐藏密码' : '显示密码'"
|
||||||
|
>
|
||||||
<i v-if="passwordVisible" class="fa-regular fa-eye"></i>
|
<i v-if="passwordVisible" class="fa-regular fa-eye"></i>
|
||||||
<i v-else class="fa-solid fa-eye-slash"></i>
|
<i v-else class="fa-solid fa-eye-slash"></i>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="openVerifyEnabled && verifyType === 'captcha'" class="form-group code-row">
|
<div
|
||||||
<input v-model="captchaInput" type="text" placeholder="请输入4位验证码" class="input code-input" />
|
v-if="openVerifyEnabled && verifyType === 'captcha'"
|
||||||
<button class="code-btn" type="button" @click="generateCaptcha">{{ captchaText }}</button>
|
class="form-group code-row"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="captchaInput"
|
||||||
|
type="text"
|
||||||
|
placeholder="请输入4位验证码"
|
||||||
|
class="input code-input"
|
||||||
|
/>
|
||||||
|
<button class="code-btn" type="button" @click="generateCaptcha">
|
||||||
|
{{ captchaText }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="openVerifyEnabled && (verifyType === 'sms' || verifyType === 'email')" class="form-group code-row">
|
<div
|
||||||
<input v-model="verifyCode" type="text" placeholder="请输入验证码" class="input code-input" />
|
v-if="
|
||||||
<button class="code-btn" type="button" :disabled="codeSending || codeCountdown > 0" @click="handleSendCode">
|
openVerifyEnabled &&
|
||||||
{{ codeCountdown > 0 ? `${codeCountdown}s` : (codeSending ? "发送中..." : "发送验证码") }}
|
(verifyType === 'sms' || verifyType === 'email')
|
||||||
|
"
|
||||||
|
class="form-group code-row"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="verifyCode"
|
||||||
|
type="text"
|
||||||
|
placeholder="请输入验证码"
|
||||||
|
class="input code-input"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="code-btn"
|
||||||
|
type="button"
|
||||||
|
:disabled="codeSending || codeCountdown > 0"
|
||||||
|
@click="handleSendCode"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
codeCountdown > 0
|
||||||
|
? `${codeCountdown}s`
|
||||||
|
: codeSending
|
||||||
|
? "发送中..."
|
||||||
|
: "发送验证码"
|
||||||
|
}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- 极验验证码容器 -->
|
<!-- 极验验证码容器 -->
|
||||||
<div style="display: none;" v-if="openVerifyEnabled && verifyType === 'geetest' && showCaptchaContainer" class="geetest-container" ref="captchaContainer"></div>
|
<div
|
||||||
|
style="display: none"
|
||||||
|
v-if="
|
||||||
|
openVerifyEnabled &&
|
||||||
|
verifyType === 'geetest' &&
|
||||||
|
showCaptchaContainer
|
||||||
|
"
|
||||||
|
class="geetest-container"
|
||||||
|
ref="captchaContainer"
|
||||||
|
></div>
|
||||||
|
|
||||||
<div class="remember-me-row">
|
<div class="remember-me-row">
|
||||||
<label class="remember-me-label">
|
<label class="remember-me-label">
|
||||||
<input type="checkbox" v-model="rememberMe" class="remember-me-checkbox" @change="handleRememberMeChange" />
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
v-model="rememberMe"
|
||||||
|
class="remember-me-checkbox"
|
||||||
|
@change="handleRememberMeChange"
|
||||||
|
/>
|
||||||
<span>记住我</span>
|
<span>记住我</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="action-links">
|
<div class="action-links">
|
||||||
@ -68,9 +147,7 @@
|
|||||||
<button class="login-btn" @click="handleLogin" :disabled="loading">
|
<button class="login-btn" @click="handleLogin" :disabled="loading">
|
||||||
{{ loading ? "登录中..." : "登 录" }}
|
{{ loading ? "登录中..." : "登 录" }}
|
||||||
</button>
|
</button>
|
||||||
<div>
|
<div></div>
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 背景光效 -->
|
<!-- 背景光效 -->
|
||||||
@ -83,7 +160,12 @@
|
|||||||
import { ref, onMounted, nextTick } from "vue";
|
import { ref, onMounted, nextTick } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { login, getOpenVerify, getGeetest4Infos, sendLoginCode } from "@/api/login";
|
import {
|
||||||
|
login,
|
||||||
|
getOpenVerify,
|
||||||
|
getGeetest4Infos,
|
||||||
|
sendLoginCode,
|
||||||
|
} from "@/api/login";
|
||||||
import { getVerifyInfos } from "@/api/sitesettings.js";
|
import { getVerifyInfos } from "@/api/sitesettings.js";
|
||||||
import "@/assets/js/gt4.js";
|
import "@/assets/js/gt4.js";
|
||||||
import { ElMessageBox, ElMessage } from "element-plus";
|
import { ElMessageBox, ElMessage } from "element-plus";
|
||||||
@ -115,7 +197,7 @@ const captchaInstance = ref(null);
|
|||||||
// --- 加载JS脚本 ---
|
// --- 加载JS脚本 ---
|
||||||
const loadScript = (url) => {
|
const loadScript = (url) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const script = document.createElement('script');
|
const script = document.createElement("script");
|
||||||
script.src = url;
|
script.src = url;
|
||||||
script.async = true;
|
script.async = true;
|
||||||
script.onload = () => resolve();
|
script.onload = () => resolve();
|
||||||
@ -142,7 +224,7 @@ const performLoginRequest = async () => {
|
|||||||
const res = await login({
|
const res = await login({
|
||||||
account: account.value,
|
account: account.value,
|
||||||
password: password.value,
|
password: password.value,
|
||||||
code: verifyCode.value
|
code: verifyCode.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res && res.code === 200) {
|
if (res && res.code === 200) {
|
||||||
@ -199,7 +281,10 @@ const handleSendCode = async () => {
|
|||||||
}
|
}
|
||||||
codeSending.value = true;
|
codeSending.value = true;
|
||||||
try {
|
try {
|
||||||
const res = await sendLoginCode({ account: account.value, channel: verifyType.value });
|
const res = await sendLoginCode({
|
||||||
|
account: account.value,
|
||||||
|
channel: verifyType.value,
|
||||||
|
});
|
||||||
if (res?.code === 200) {
|
if (res?.code === 200) {
|
||||||
ElMessage.success("验证码已发送");
|
ElMessage.success("验证码已发送");
|
||||||
startCodeCountdown();
|
startCodeCountdown();
|
||||||
@ -243,12 +328,14 @@ const startGeetest4 = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.initGeetest4({
|
window.initGeetest4(
|
||||||
|
{
|
||||||
captchaId: config.captcha_id,
|
captchaId: config.captcha_id,
|
||||||
product: 'bind',
|
product: "bind",
|
||||||
language: 'zh-CN',
|
language: "zh-CN",
|
||||||
container: captchaContainer.value
|
container: captchaContainer.value,
|
||||||
}, (instance) => {
|
},
|
||||||
|
(instance) => {
|
||||||
captchaInstance.value = instance;
|
captchaInstance.value = instance;
|
||||||
|
|
||||||
// 验证成功回调
|
// 验证成功回调
|
||||||
@ -262,7 +349,7 @@ const startGeetest4 = async () => {
|
|||||||
lot_number: result?.lot_number || "",
|
lot_number: result?.lot_number || "",
|
||||||
pass_token: result?.pass_token || "",
|
pass_token: result?.pass_token || "",
|
||||||
gen_time: result?.gen_time || "",
|
gen_time: result?.gen_time || "",
|
||||||
captcha_output: result?.captcha_output || ""
|
captcha_output: result?.captcha_output || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loginRes && loginRes.code === 200) {
|
if (loginRes && loginRes.code === 200) {
|
||||||
@ -307,7 +394,8 @@ const startGeetest4 = async () => {
|
|||||||
|
|
||||||
// 显示验证码
|
// 显示验证码
|
||||||
instance.showCaptcha();
|
instance.showCaptcha();
|
||||||
});
|
},
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -339,7 +427,10 @@ const handleLogin = async () => {
|
|||||||
verifyType.value = typeItem?.value || "captcha";
|
verifyType.value = typeItem?.value || "captcha";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (openVerifyEnabled.value && (verifyType.value === "sms" || verifyType.value === "email")) {
|
if (
|
||||||
|
openVerifyEnabled.value &&
|
||||||
|
(verifyType.value === "sms" || verifyType.value === "email")
|
||||||
|
) {
|
||||||
if (!verifyCode.value.trim()) {
|
if (!verifyCode.value.trim()) {
|
||||||
errorMsg.value = "请输入验证码";
|
errorMsg.value = "请输入验证码";
|
||||||
return;
|
return;
|
||||||
@ -365,7 +456,8 @@ const handleLogin = async () => {
|
|||||||
await performLoginRequest();
|
await performLoginRequest();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errorMsg.value = err?.response?.data?.msg || err?.message || "登录失败,请重试";
|
errorMsg.value =
|
||||||
|
err?.response?.data?.msg || err?.message || "登录失败,请重试";
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
@ -376,14 +468,14 @@ const handleRememberMeChange = async () => {
|
|||||||
if (rememberMe.value) {
|
if (rememberMe.value) {
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(
|
await ElMessageBox.confirm(
|
||||||
'请确认电脑环境是可信的,登录成功后会自动记住密码。',
|
"请确认电脑环境是可信的,登录成功后会自动记住密码。",
|
||||||
'安全提示',
|
"安全提示",
|
||||||
{
|
{
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: "确定",
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: "取消",
|
||||||
type: 'warning',
|
type: "warning",
|
||||||
closeOnClickModal: false
|
closeOnClickModal: false,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
rememberMe.value = false;
|
rememberMe.value = false;
|
||||||
@ -405,16 +497,21 @@ onMounted(() => {
|
|||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res?.code === 200 && res?.data?.use_geetest) {
|
if (res?.code === 200 && res?.data?.use_geetest) {
|
||||||
verifyType.value = res.data.use_geetest;
|
verifyType.value = res.data.use_geetest;
|
||||||
openVerifyEnabled.value = Number(res.data.openVerify_enabled ?? 1) === 1;
|
openVerifyEnabled.value =
|
||||||
|
Number(res.data.openVerify_enabled ?? 1) === 1;
|
||||||
} else {
|
} else {
|
||||||
// 兼容旧接口
|
// 兼容旧接口
|
||||||
return getOpenVerify().then((verifyRes) => {
|
return getOpenVerify().then((verifyRes) => {
|
||||||
if (verifyRes?.code === 200 && Array.isArray(verifyRes?.data)) {
|
if (verifyRes?.code === 200 && Array.isArray(verifyRes?.data)) {
|
||||||
const openItem = verifyRes.data.find((i) => i.label === "openVerify");
|
const openItem = verifyRes.data.find(
|
||||||
|
(i) => i.label === "openVerify",
|
||||||
|
);
|
||||||
if (openItem?.value !== undefined) {
|
if (openItem?.value !== undefined) {
|
||||||
openVerifyEnabled.value = String(openItem.value) === "1";
|
openVerifyEnabled.value = String(openItem.value) === "1";
|
||||||
}
|
}
|
||||||
const typeItem = verifyRes.data.find((i) => i.label === "verifyType");
|
const typeItem = verifyRes.data.find(
|
||||||
|
(i) => i.label === "verifyType",
|
||||||
|
);
|
||||||
if (typeItem?.value) verifyType.value = typeItem.value;
|
if (typeItem?.value) verifyType.value = typeItem.value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -279,6 +279,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed, onMounted } from "vue";
|
import { ref, computed, onMounted } from "vue";
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
|
import { getFileUrl as getFileUrlUtil } from "@/utils/url";
|
||||||
import {
|
import {
|
||||||
Folder,
|
Folder,
|
||||||
Grid,
|
Grid,
|
||||||
@ -412,7 +413,7 @@ const getUserCateData = async () => {
|
|||||||
|
|
||||||
//图片拼接接口地址
|
//图片拼接接口地址
|
||||||
const getFileUrl = (url: string) => {
|
const getFileUrl = (url: string) => {
|
||||||
return `${import.meta.env.VITE_API_BASE_URL}${url}`;
|
return getFileUrlUtil(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 未分类分组(固定,不可编辑)
|
// 未分类分组(固定,不可编辑)
|
||||||
|
|||||||
288
src/views/system/platformsettings/components/storageSettings.vue
Normal file
288
src/views/system/platformsettings/components/storageSettings.vue
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
<template>
|
||||||
|
<div class="storage-settings">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="rules"
|
||||||
|
label-width="140px"
|
||||||
|
class="settings-form"
|
||||||
|
>
|
||||||
|
<el-card shadow="never" class="settings-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>存储位置配置</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-form-item label="存储类型" prop="storage_type">
|
||||||
|
<el-radio-group v-model="formData.storage_type">
|
||||||
|
<el-radio label="local">本地存储</el-radio>
|
||||||
|
<el-radio label="qiniu">七牛云存储</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<template v-if="formData.storage_type === 'qiniu'">
|
||||||
|
<el-divider content-position="left">七牛云配置</el-divider>
|
||||||
|
|
||||||
|
<el-form-item label="AccessKey" prop="qiniu_access_key">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.qiniu_access_key"
|
||||||
|
placeholder="请输入七牛云 AccessKey"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="SecretKey" prop="qiniu_secret_key">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.qiniu_secret_key"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入七牛云 SecretKey"
|
||||||
|
show-password
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="Bucket" prop="qiniu_bucket">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.qiniu_bucket"
|
||||||
|
placeholder="请输入存储空间名称"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="CDN域名" prop="qiniu_domain">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.qiniu_domain"
|
||||||
|
placeholder="请输入CDN加速域名,如:https://cdn.example.com"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<el-icon><Link /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<div class="form-tip">用于访问上传的文件,需要在七牛云控制台配置</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="存储区域" prop="qiniu_region">
|
||||||
|
<el-select
|
||||||
|
v-model="formData.qiniu_region"
|
||||||
|
placeholder="请选择存储区域"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<el-option label="华东-浙江" value="z0" />
|
||||||
|
<el-option label="华北-河北" value="z1" />
|
||||||
|
<el-option label="华南-广东" value="z2" />
|
||||||
|
<el-option label="北美-洛杉矶" value="na0" />
|
||||||
|
<el-option label="亚太-新加坡" value="as0" />
|
||||||
|
<el-option label="华东-浙江2" value="cn-east-2" />
|
||||||
|
</el-select>
|
||||||
|
<div class="form-tip">请根据您的Bucket所在区域选择</div>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-alert
|
||||||
|
v-if="formData.storage_type === 'local'"
|
||||||
|
title="本地存储说明"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
>
|
||||||
|
文件将存储在服务器本地磁盘,适合小规模应用或开发测试环境
|
||||||
|
</el-alert>
|
||||||
|
|
||||||
|
<el-alert
|
||||||
|
v-if="formData.storage_type === 'qiniu'"
|
||||||
|
title="七牛云存储说明"
|
||||||
|
type="warning"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
>
|
||||||
|
<p>使用七牛云存储需要:</p>
|
||||||
|
<ul style="margin: 8px 0; padding-left: 20px;">
|
||||||
|
<li>在七牛云控制台创建存储空间(Bucket)</li>
|
||||||
|
<li>获取 AccessKey 和 SecretKey(在个人中心-密钥管理)</li>
|
||||||
|
<li>配置 CDN 加速域名(在存储空间-域名管理)</li>
|
||||||
|
<li>确保域名已备案并完成 CNAME 解析</li>
|
||||||
|
</ul>
|
||||||
|
</el-alert>
|
||||||
|
</el-card>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<div class="footer-actions">
|
||||||
|
<el-button @click="handleReset">重置</el-button>
|
||||||
|
<el-button type="primary" :loading="submitting" @click="handleSubmit">
|
||||||
|
保存设置
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, reactive, ref } from "vue";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import { Link } from "@element-plus/icons-vue";
|
||||||
|
import { getStorageConfig, saveStorageConfig } from "@/api/sitesettings";
|
||||||
|
|
||||||
|
const STORAGE_KEY = "storage_settings_draft";
|
||||||
|
|
||||||
|
const formRef = ref();
|
||||||
|
const submitting = ref(false);
|
||||||
|
|
||||||
|
const formData = reactive({
|
||||||
|
storage_type: "local",
|
||||||
|
qiniu_access_key: "",
|
||||||
|
qiniu_secret_key: "",
|
||||||
|
qiniu_bucket: "",
|
||||||
|
qiniu_domain: "",
|
||||||
|
qiniu_region: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const validateQiniuField = (field, fieldName) => {
|
||||||
|
return (_rule, value, callback) => {
|
||||||
|
if (
|
||||||
|
formData.storage_type === "qiniu" &&
|
||||||
|
!String(value || "").trim()
|
||||||
|
) {
|
||||||
|
callback(new Error(`${fieldName}不能为空`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
qiniu_access_key: [
|
||||||
|
{ validator: validateQiniuField("qiniu_access_key", "AccessKey"), trigger: "blur" },
|
||||||
|
],
|
||||||
|
qiniu_secret_key: [
|
||||||
|
{ validator: validateQiniuField("qiniu_secret_key", "SecretKey"), trigger: "blur" },
|
||||||
|
],
|
||||||
|
qiniu_bucket: [
|
||||||
|
{ validator: validateQiniuField("qiniu_bucket", "Bucket"), trigger: "blur" },
|
||||||
|
],
|
||||||
|
qiniu_domain: [
|
||||||
|
{ validator: validateQiniuField("qiniu_domain", "CDN域名"), trigger: "blur" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadDraft = () => {
|
||||||
|
const raw = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (!raw) return;
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(raw);
|
||||||
|
Object.assign(formData, data);
|
||||||
|
} catch {
|
||||||
|
// ignore invalid cache
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadRemoteConfig = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getStorageConfig();
|
||||||
|
if (res?.code === 200 && res?.data) {
|
||||||
|
Object.assign(formData, {
|
||||||
|
storage_type: res.data.storage_type || "local",
|
||||||
|
qiniu_access_key: res.data.qiniu_access_key || "",
|
||||||
|
qiniu_secret_key: res.data.qiniu_secret_key || "",
|
||||||
|
qiniu_bucket: res.data.qiniu_bucket || "",
|
||||||
|
qiniu_domain: res.data.qiniu_domain || "",
|
||||||
|
qiniu_region: res.data.qiniu_region || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("加载存储配置失败:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveDraft = () => {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(formData));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
Object.assign(formData, {
|
||||||
|
storage_type: "local",
|
||||||
|
qiniu_access_key: "",
|
||||||
|
qiniu_secret_key: "",
|
||||||
|
qiniu_bucket: "",
|
||||||
|
qiniu_domain: "",
|
||||||
|
qiniu_region: "",
|
||||||
|
});
|
||||||
|
saveDraft();
|
||||||
|
ElMessage.success("已重置");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!formRef.value) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await formRef.value.validate();
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitting.value = true;
|
||||||
|
try {
|
||||||
|
await saveStorageConfig({
|
||||||
|
storage_type: formData.storage_type,
|
||||||
|
qiniu_access_key: formData.qiniu_access_key || null,
|
||||||
|
qiniu_secret_key: formData.qiniu_secret_key || null,
|
||||||
|
qiniu_bucket: formData.qiniu_bucket || null,
|
||||||
|
qiniu_domain: formData.qiniu_domain || null,
|
||||||
|
qiniu_region: formData.qiniu_region || null,
|
||||||
|
});
|
||||||
|
saveDraft();
|
||||||
|
ElMessage.success("保存成功");
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error?.response?.data?.msg || "保存失败");
|
||||||
|
} finally {
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadDraft();
|
||||||
|
loadRemoteConfig();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.storage-settings {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-card {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-tip {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
margin-top: 4px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-alert) {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-alert ul) {
|
||||||
|
list-style-type: disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-alert li) {
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -8,12 +8,21 @@
|
|||||||
|
|
||||||
<div class="settings-container">
|
<div class="settings-container">
|
||||||
<el-tabs v-model="activeTab" class="settings-tabs">
|
<el-tabs v-model="activeTab" class="settings-tabs">
|
||||||
|
<!-- 验证开关 -->
|
||||||
<el-tab-pane label="验证开关" name="platform">
|
<el-tab-pane label="验证开关" name="platform">
|
||||||
<platformSettings
|
<platformSettings
|
||||||
ref="platformSettingsRef"
|
ref="platformSettingsRef"
|
||||||
v-if="activeTab === 'platform'"
|
v-if="activeTab === 'platform'"
|
||||||
/>
|
/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<!-- 存储配置 -->
|
||||||
|
<el-tab-pane label="存储配置" name="storage">
|
||||||
|
<storageSettings
|
||||||
|
ref="storageSettingsRef"
|
||||||
|
v-if="activeTab === 'storage'"
|
||||||
|
/>
|
||||||
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -22,6 +31,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import platformSettings from "./components/platformSettings.vue";
|
import platformSettings from "./components/platformSettings.vue";
|
||||||
|
import storageSettings from "./components/storageSettings.vue";
|
||||||
|
|
||||||
const activeTab = ref("platform");
|
const activeTab = ref("platform");
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user