增加七牛云存储
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
|
||||
@ -126,4 +126,28 @@ export function saveCompanySeo(data) {
|
||||
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,
|
||||
});
|
||||
}
|
||||
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-side">
|
||||
<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>
|
||||
</div>
|
||||
<div class="illus">
|
||||
<svg viewBox="0 0 300 160" style="max-width: 100%" fill="none">
|
||||
<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="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" />
|
||||
<rect
|
||||
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>
|
||||
</div>
|
||||
<!-- 版权信息 -->
|
||||
@ -24,36 +43,96 @@
|
||||
<span class="input-icon">
|
||||
<i class="fa-solid fa-user"></i>
|
||||
</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 class="form-group icon-input-group">
|
||||
<span class="input-icon">
|
||||
<i class="fa-solid fa-lock"></i>
|
||||
</span>
|
||||
<input v-model="password" :type="passwordVisible ? 'text' : 'password'" placeholder="密码"
|
||||
autocomplete="current-password" class="input input-with-icon" />
|
||||
<span class="visible-btn" @click="passwordVisible = !passwordVisible"
|
||||
:title="passwordVisible ? '隐藏密码' : '显示密码'">
|
||||
<input
|
||||
v-model="password"
|
||||
:type="passwordVisible ? 'text' : 'password'"
|
||||
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-else class="fa-solid fa-eye-slash"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="openVerifyEnabled && verifyType === 'captcha'" 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
|
||||
v-if="openVerifyEnabled && verifyType === 'captcha'"
|
||||
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 v-if="openVerifyEnabled && (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 ? "发送中..." : "发送验证码") }}
|
||||
<div
|
||||
v-if="
|
||||
openVerifyEnabled &&
|
||||
(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>
|
||||
</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">
|
||||
<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>
|
||||
</label>
|
||||
<div class="action-links">
|
||||
@ -68,9 +147,7 @@
|
||||
<button class="login-btn" @click="handleLogin" :disabled="loading">
|
||||
{{ loading ? "登录中..." : "登 录" }}
|
||||
</button>
|
||||
<div>
|
||||
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 背景光效 -->
|
||||
@ -83,7 +160,12 @@
|
||||
import { ref, onMounted, nextTick } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
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 "@/assets/js/gt4.js";
|
||||
import { ElMessageBox, ElMessage } from "element-plus";
|
||||
@ -115,7 +197,7 @@ const captchaInstance = ref(null);
|
||||
// --- 加载JS脚本 ---
|
||||
const loadScript = (url) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
const script = document.createElement("script");
|
||||
script.src = url;
|
||||
script.async = true;
|
||||
script.onload = () => resolve();
|
||||
@ -142,7 +224,7 @@ const performLoginRequest = async () => {
|
||||
const res = await login({
|
||||
account: account.value,
|
||||
password: password.value,
|
||||
code: verifyCode.value
|
||||
code: verifyCode.value,
|
||||
});
|
||||
|
||||
if (res && res.code === 200) {
|
||||
@ -199,7 +281,10 @@ const handleSendCode = async () => {
|
||||
}
|
||||
codeSending.value = true;
|
||||
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) {
|
||||
ElMessage.success("验证码已发送");
|
||||
startCodeCountdown();
|
||||
@ -243,71 +328,74 @@ const startGeetest4 = async () => {
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
window.initGeetest4({
|
||||
captchaId: config.captcha_id,
|
||||
product: 'bind',
|
||||
language: 'zh-CN',
|
||||
container: captchaContainer.value
|
||||
}, (instance) => {
|
||||
captchaInstance.value = instance;
|
||||
window.initGeetest4(
|
||||
{
|
||||
captchaId: config.captcha_id,
|
||||
product: "bind",
|
||||
language: "zh-CN",
|
||||
container: captchaContainer.value,
|
||||
},
|
||||
(instance) => {
|
||||
captchaInstance.value = instance;
|
||||
|
||||
// 验证成功回调
|
||||
instance.onSuccess(async () => {
|
||||
const result = instance.getValidate();
|
||||
// 将验证结果添加到登录参数中
|
||||
const loginRes = await login({
|
||||
account: account.value,
|
||||
password: password.value,
|
||||
captcha_id: result?.captcha_id || "",
|
||||
lot_number: result?.lot_number || "",
|
||||
pass_token: result?.pass_token || "",
|
||||
gen_time: result?.gen_time || "",
|
||||
captcha_output: result?.captcha_output || ""
|
||||
// 验证成功回调
|
||||
instance.onSuccess(async () => {
|
||||
const result = instance.getValidate();
|
||||
// 将验证结果添加到登录参数中
|
||||
const loginRes = await login({
|
||||
account: account.value,
|
||||
password: password.value,
|
||||
captcha_id: result?.captcha_id || "",
|
||||
lot_number: result?.lot_number || "",
|
||||
pass_token: result?.pass_token || "",
|
||||
gen_time: result?.gen_time || "",
|
||||
captcha_output: result?.captcha_output || "",
|
||||
});
|
||||
|
||||
if (loginRes && loginRes.code === 200) {
|
||||
if (rememberMe.value) {
|
||||
localStorage.setItem("loginAccount", account.value);
|
||||
localStorage.setItem("loginPassword", password.value);
|
||||
localStorage.setItem("loginRememberMe", "true");
|
||||
} else {
|
||||
localStorage.removeItem("loginAccount");
|
||||
localStorage.removeItem("loginTenantName");
|
||||
localStorage.removeItem("loginPassword");
|
||||
localStorage.setItem("loginRememberMe", "false");
|
||||
}
|
||||
|
||||
authStore.setLoginInfo(loginRes.data);
|
||||
const { useTabsStore } = await import("@/stores");
|
||||
const tabsStore = useTabsStore();
|
||||
tabsStore.resetTabs();
|
||||
router.push({ path: "/home" });
|
||||
ElMessage.success("登录成功!");
|
||||
} else {
|
||||
errorMsg.value = loginRes.msg || "登录失败";
|
||||
}
|
||||
loading.value = false;
|
||||
cleanCaptchaInstance();
|
||||
});
|
||||
|
||||
if (loginRes && loginRes.code === 200) {
|
||||
if (rememberMe.value) {
|
||||
localStorage.setItem("loginAccount", account.value);
|
||||
localStorage.setItem("loginPassword", password.value);
|
||||
localStorage.setItem("loginRememberMe", "true");
|
||||
} else {
|
||||
localStorage.removeItem("loginAccount");
|
||||
localStorage.removeItem("loginTenantName");
|
||||
localStorage.removeItem("loginPassword");
|
||||
localStorage.setItem("loginRememberMe", "false");
|
||||
}
|
||||
// 验证失败回调
|
||||
instance.onFail(() => {
|
||||
errorMsg.value = "验证码验证失败,请重试";
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
authStore.setLoginInfo(loginRes.data);
|
||||
const { useTabsStore } = await import("@/stores");
|
||||
const tabsStore = useTabsStore();
|
||||
tabsStore.resetTabs();
|
||||
router.push({ path: "/home" });
|
||||
ElMessage.success("登录成功!");
|
||||
} else {
|
||||
errorMsg.value = loginRes.msg || "登录失败";
|
||||
}
|
||||
loading.value = false;
|
||||
cleanCaptchaInstance();
|
||||
});
|
||||
// 错误回调 - 网络错误时跳过验证直接登录
|
||||
instance.onError((err) => {
|
||||
errorMsg.value = "验证码加载失败,跳过验证直接登录";
|
||||
loading.value = false;
|
||||
cleanCaptchaInstance();
|
||||
// 跳过验证直接登录
|
||||
performLoginRequest();
|
||||
});
|
||||
|
||||
// 验证失败回调
|
||||
instance.onFail(() => {
|
||||
errorMsg.value = "验证码验证失败,请重试";
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
// 错误回调 - 网络错误时跳过验证直接登录
|
||||
instance.onError((err) => {
|
||||
errorMsg.value = "验证码加载失败,跳过验证直接登录";
|
||||
loading.value = false;
|
||||
cleanCaptchaInstance();
|
||||
// 跳过验证直接登录
|
||||
performLoginRequest();
|
||||
});
|
||||
|
||||
// 显示验证码
|
||||
instance.showCaptcha();
|
||||
});
|
||||
// 显示验证码
|
||||
instance.showCaptcha();
|
||||
},
|
||||
);
|
||||
return true;
|
||||
};
|
||||
|
||||
@ -339,7 +427,10 @@ const handleLogin = async () => {
|
||||
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()) {
|
||||
errorMsg.value = "请输入验证码";
|
||||
return;
|
||||
@ -365,7 +456,8 @@ const handleLogin = async () => {
|
||||
await performLoginRequest();
|
||||
}
|
||||
} catch (err) {
|
||||
errorMsg.value = err?.response?.data?.msg || err?.message || "登录失败,请重试";
|
||||
errorMsg.value =
|
||||
err?.response?.data?.msg || err?.message || "登录失败,请重试";
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@ -376,14 +468,14 @@ const handleRememberMeChange = async () => {
|
||||
if (rememberMe.value) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'请确认电脑环境是可信的,登录成功后会自动记住密码。',
|
||||
'安全提示',
|
||||
"请确认电脑环境是可信的,登录成功后会自动记住密码。",
|
||||
"安全提示",
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
closeOnClickModal: false
|
||||
}
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
closeOnClickModal: false,
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
rememberMe.value = false;
|
||||
@ -405,16 +497,21 @@ onMounted(() => {
|
||||
.then((res) => {
|
||||
if (res?.code === 200 && 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 {
|
||||
// 兼容旧接口
|
||||
return getOpenVerify().then((verifyRes) => {
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
});
|
||||
@ -859,4 +956,4 @@ const clearCache = async () => {
|
||||
padding-top: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -279,6 +279,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { getFileUrl as getFileUrlUtil } from "@/utils/url";
|
||||
import {
|
||||
Folder,
|
||||
Grid,
|
||||
@ -412,7 +413,7 @@ const getUserCateData = async () => {
|
||||
|
||||
//图片拼接接口地址
|
||||
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">
|
||||
<el-tabs v-model="activeTab" class="settings-tabs">
|
||||
<!-- 验证开关 -->
|
||||
<el-tab-pane label="验证开关" name="platform">
|
||||
<platformSettings
|
||||
ref="platformSettingsRef"
|
||||
v-if="activeTab === 'platform'"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 存储配置 -->
|
||||
<el-tab-pane label="存储配置" name="storage">
|
||||
<storageSettings
|
||||
ref="storageSettingsRef"
|
||||
v-if="activeTab === 'storage'"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
@ -22,6 +31,8 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import platformSettings from "./components/platformSettings.vue";
|
||||
import storageSettings from "./components/storageSettings.vue";
|
||||
|
||||
const activeTab = ref("platform");
|
||||
</script>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user