mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-22 22:21:09 +08:00
Compare commits
22 Commits
codex/upda
...
codex/upda
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbb0a11d49 | ||
|
|
35340319c6 | ||
|
|
343c4d3793 | ||
|
|
87b214cbc0 | ||
|
|
e7f06787d2 | ||
|
|
d7d2fd5dcb | ||
|
|
76b65a1400 | ||
|
|
fa8ee113a2 | ||
|
|
181237adee | ||
|
|
1b8135acfb | ||
|
|
67bbe832a0 | ||
|
|
9d67f7d8d6 | ||
|
|
da0d26c8b5 | ||
|
|
81d64bfc7b | ||
|
|
3e255c1288 | ||
|
|
224e1a1018 | ||
|
|
4456997573 | ||
|
|
ef0f0d013b | ||
|
|
a83ddc40fe | ||
|
|
f36ed28185 | ||
|
|
1d31284dba | ||
|
|
995d68b50b |
23
.env.example
23
.env.example
@@ -80,26 +80,39 @@ WEBPUSH_PRIVATE_KEY=
|
|||||||
LOG_LEVEL=INFO
|
LOG_LEVEL=INFO
|
||||||
|
|
||||||
# === Frontend (Nuxt) ===
|
# === Frontend (Nuxt) ===
|
||||||
|
# 本地开发
|
||||||
NUXT_PUBLIC_API_BASE_URL=http://localhost:8080
|
NUXT_PUBLIC_API_BASE_URL=http://localhost:8080
|
||||||
|
# 线上环境
|
||||||
# NUXT_PUBLIC_API_BASE_URL=https://www.open-isle.com
|
# NUXT_PUBLIC_API_BASE_URL=https://www.open-isle.com
|
||||||
|
# 测试环境
|
||||||
# NUXT_PUBLIC_API_BASE_URL=https://www.staging.open-isle.com
|
# NUXT_PUBLIC_API_BASE_URL=https://www.staging.open-isle.com
|
||||||
|
|
||||||
|
# 本地开发
|
||||||
NUXT_PUBLIC_WEBSOCKET_URL=http://localhost:8082
|
NUXT_PUBLIC_WEBSOCKET_URL=http://localhost:8082
|
||||||
# NUXT_PUBLIC_WEBSOCKET_URL=https://www.open-isle.com
|
# 线上环境
|
||||||
# NUXT_PUBLIC_WEBSOCKET_URL=https://www.staging.open-isle.com
|
# NUXT_PUBLIC_WEBSOCKET_URL=https://www.open-isle.com/websocket
|
||||||
|
# 测试环境
|
||||||
|
# NUXT_PUBLIC_WEBSOCKET_URL=https://www.staging.open-isle.com/websocket
|
||||||
|
|
||||||
|
# 本地开发
|
||||||
NUXT_PUBLIC_WEBSITE_BASE_URL=http://localhost:3000
|
NUXT_PUBLIC_WEBSITE_BASE_URL=http://localhost:3000
|
||||||
# 线上 & 本地均可使用
|
# 线上 & 测试 (www.staging.open-isle.com) & 本地均可使用
|
||||||
NUXT_PUBLIC_GOOGLE_CLIENT_ID=777830451304-nt8afkkap18gui4f9entcha99unal744.apps.googleusercontent.com
|
NUXT_PUBLIC_GOOGLE_CLIENT_ID=777830451304-nt8afkkap18gui4f9entcha99unal744.apps.googleusercontent.com
|
||||||
# 线上
|
# 线上
|
||||||
NUXT_PUBLIC_GITHUB_CLIENT_ID=Ov23liOlrZnPKRF7s7NN
|
NUXT_PUBLIC_GITHUB_CLIENT_ID=Ov23liVkO1NPAX5JyWxJ
|
||||||
|
# 测试环境 (www.staging.open-isle.com)
|
||||||
|
# NUXT_PUBLIC_GITHUB_CLIENT_ID=Ov23li6GHPxx4MwipWnM
|
||||||
# 本地
|
# 本地
|
||||||
# NUXT_PUBLIC_GITHUB_CLIENT_ID=Ov23liOlrZnPKRF7s7NN
|
# NUXT_PUBLIC_GITHUB_CLIENT_ID=Ov23liOlrZnPKRF7s7NN
|
||||||
|
|
||||||
# 线上 & 本地均可使用
|
# 线上 & 本地均可使用
|
||||||
NUXT_PUBLIC_DISCORD_CLIENT_ID=1394985417044000779
|
NUXT_PUBLIC_DISCORD_CLIENT_ID=1394985417044000779
|
||||||
|
|
||||||
# 线上 & 本地均可使用
|
# 线上 & 本地均可使用
|
||||||
NUXT_PUBLIC_TWITTER_CLIENT_ID=ZTRTU05KSk9KTTJrTTdrVC1tc1E6MTpjaQ
|
NUXT_PUBLIC_TWITTER_CLIENT_ID=ZTRTU05KSk9KTTJrTTdrVC1tc1E6MTpjaQ
|
||||||
|
|
||||||
# 线上
|
# 线上
|
||||||
NUXT_PUBLIC_TELEGRAM_BOT_ID=8450237135
|
NUXT_PUBLIC_TELEGRAM_BOT_ID=8450237135
|
||||||
|
# 测试环境 (www.staging.open-isle.com)
|
||||||
|
# NUXT_PUBLIC_TELEGRAM_BOT_ID=7832207011
|
||||||
|
|
||||||
|
|||||||
@@ -34,15 +34,11 @@ cd OpenIsle
|
|||||||
想要最快速地同时体验前端和后端,可直接使用仓库提供的 Docker Compose。该方案会一次性拉起数据库、消息队列、搜索、后端、WebSocket 以及前端 Dev Server,适合需要全链路联调的场景。
|
想要最快速地同时体验前端和后端,可直接使用仓库提供的 Docker Compose。该方案会一次性拉起数据库、消息队列、搜索、后端、WebSocket 以及前端 Dev Server,适合需要全链路联调的场景。
|
||||||
|
|
||||||
1. 准备环境变量文件:
|
1. 准备环境变量文件:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
```
|
```
|
||||||
|
|
||||||
`.env.example` 是模板,可在 `.env` 中按需覆盖如端口、密钥等配置。确保 `NUXT_PUBLIC_API_BASE_URL`、`NUXT_PUBLIC_WEBSOCKET_URL` 等仍指向 `localhost`,方便前端直接访问容器映射端口。
|
`.env.example` 是模板,可在 `.env` 中按需覆盖如端口、密钥等配置。确保 `NUXT_PUBLIC_API_BASE_URL`、`NUXT_PUBLIC_WEBSOCKET_URL` 等仍指向 `localhost`,方便前端直接访问容器映射端口。
|
||||||
|
|
||||||
2. 启动 Dev Profile:
|
2. 启动 Dev Profile:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker compose \
|
docker compose \
|
||||||
-f docker/docker-compose.yaml \
|
-f docker/docker-compose.yaml \
|
||||||
@@ -56,7 +52,6 @@ cd OpenIsle
|
|||||||
--env-file .env \
|
--env-file .env \
|
||||||
--profile dev up -d
|
--profile dev up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
该命令会创建名为 `frontend_dev` 的容器并运行 `npm run dev`,浏览器访问 http://127.0.0.1:3000 即可查看页面。
|
该命令会创建名为 `frontend_dev` 的容器并运行 `npm run dev`,浏览器访问 http://127.0.0.1:3000 即可查看页面。
|
||||||
|
|
||||||
修改代码后,可以强制重新创建所有容器,执行:
|
修改代码后,可以强制重新创建所有容器,执行:
|
||||||
@@ -69,14 +64,11 @@ cd OpenIsle
|
|||||||
```
|
```
|
||||||
|
|
||||||
3. 查看服务状态:
|
3. 查看服务状态:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker compose -f docker/docker-compose.yaml --env-file .env ps
|
docker compose -f docker/docker-compose.yaml --env-file .env ps
|
||||||
docker compose -f docker/docker-compose.yaml --env-file .env logs -f frontend_dev
|
docker compose -f docker/docker-compose.yaml --env-file .env logs -f frontend_dev
|
||||||
```
|
```
|
||||||
|
|
||||||
4. 停止所有容器:
|
4. 停止所有容器:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker compose -f docker/docker-compose.yaml --env-file .env --profile dev down
|
docker compose -f docker/docker-compose.yaml --env-file .env --profile dev down
|
||||||
```
|
```
|
||||||
@@ -100,8 +92,13 @@ IDEA 打开 `backend/` 文件夹。
|
|||||||
|
|
||||||
#### 配置环境变量
|
#### 配置环境变量
|
||||||
|
|
||||||
1. 在 IDEA 中配置「Environment file」:将 `Run/Debug Configuration` 的 `Environment variables` 指向刚刚复制的 `.env`,即可让 IDE 读取该文件。
|
1. 生成环境变量文件:
|
||||||
2. 需要调整端口或功能开关时,优先修改 `.env`,例如:
|
```shell
|
||||||
|
cp open-isle.env.example open-isle.env
|
||||||
|
```
|
||||||
|
`open-isle.env` 才是实际被读取的文件。可在其中补充数据库、第三方服务等配置,`open-isle.env` 已被 Git 忽略,放心修改。
|
||||||
|
2. 在 IDEA 中配置「Environment file」:将 `Run/Debug Configuration` 的 `Environment variables` 指向刚刚复制的 `open-isle.env`,即可让 IDE 读取该文件。
|
||||||
|
3. 需要调整端口或功能开关时,优先修改 `open-isle.env`,例如:
|
||||||
```ini
|
```ini
|
||||||
SERVER_PORT=8081
|
SERVER_PORT=8081
|
||||||
LOG_LEVEL=DEBUG
|
LOG_LEVEL=DEBUG
|
||||||
@@ -109,7 +106,7 @@ IDEA 打开 `backend/` 文件夹。
|
|||||||
|
|
||||||
也可以修改 `src/main/resources/application.properties`,但该文件会被 Git 追踪,通常不推荐。
|
也可以修改 `src/main/resources/application.properties`,但该文件会被 Git 追踪,通常不推荐。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### 配置 IDEA 参数
|
#### 配置 IDEA 参数
|
||||||
|
|
||||||
@@ -119,33 +116,15 @@ IDEA 打开 `backend/` 文件夹。
|
|||||||
-Dserver.port=8081
|
-Dserver.port=8081
|
||||||
```
|
```
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
完成环境变量和运行参数设置后,即可启动 Spring Boot 应用。
|
完成环境变量和运行参数设置后,即可启动 Spring Boot 应用。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 启动前端服务
|
## 前端连接预发或正式环境
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> **⚠️ 环境要求:Node.js 版本最低 20.0.0(因为 Nuxt 框架要求)**
|
|
||||||
|
|
||||||
```shell
|
|
||||||
cd frontend_nuxt/
|
|
||||||
```
|
|
||||||
|
|
||||||
安装依赖并启动开发服务器:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
npm install --verbose
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
默认情况下,浏览器访问 http://127.0.0.1:3000 即可访问前端页面。
|
|
||||||
|
|
||||||
### 连接预发或正式环境
|
|
||||||
|
|
||||||
前端默认读取 `.env` 中的接口地址,可通过修改以下变量快速切换到预发或正式环境:
|
前端默认读取 `.env` 中的接口地址,可通过修改以下变量快速切换到预发或正式环境:
|
||||||
|
|
||||||
@@ -155,10 +134,8 @@ npm run dev
|
|||||||
NUXT_PUBLIC_API_BASE_URL=https://www.staging.open-isle.com
|
NUXT_PUBLIC_API_BASE_URL=https://www.staging.open-isle.com
|
||||||
NUXT_PUBLIC_WEBSOCKET_URL=https://www.staging.open-isle.com
|
NUXT_PUBLIC_WEBSOCKET_URL=https://www.staging.open-isle.com
|
||||||
```
|
```
|
||||||
|
|
||||||
将 `staging` 替换为 `www` 即可连接正式环境。其他变量(如 OAuth Client ID、站点地址等)可根据需求调整。
|
将 `staging` 替换为 `www` 即可连接正式环境。其他变量(如 OAuth Client ID、站点地址等)可根据需求调整。
|
||||||
|
|
||||||
2. 已经存在 `.env` 时,可直接编辑上述变量并重启 `npm run dev` 让配置生效。
|
|
||||||
|
|
||||||
## 其他配置
|
## 其他配置
|
||||||
|
|
||||||
@@ -166,41 +143,42 @@ npm run dev
|
|||||||
|
|
||||||
- 修改 `application.properties` 配置
|
- 修改 `application.properties` 配置
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
- 修改 `.env` 配置
|
- 修改 `.env` 配置
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
- 配置第三方登录回调地址
|
- 配置第三方登录回调地址
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### 配置Resend邮箱服务
|
### 配置Resend邮箱服务
|
||||||
|
|
||||||
https://resend.com/emails 创建账号并登录
|
https://resend.com/emails 创建账号并登录
|
||||||
|
|
||||||
- `Domains` -> `Add Domain`
|
- `Domains` -> `Add Domain`
|
||||||

|

|
||||||
|
|
||||||
- 填写域名
|
- 填写域名
|
||||||

|

|
||||||
|
|
||||||
- 等待一段时间后解析成功,创建 key
|
- 等待一段时间后解析成功,创建 key
|
||||||
`API Keys` -> `Create API Key`,输入名称,设置 `Permission` 为 `Sending access`
|
`API Keys` -> `Create API Key`,输入名称,设置 `Permission` 为 `Sending access`
|
||||||
**Key 只能查看一次,务必保存下来**
|
**Key 只能查看一次,务必保存下来**
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
- 修改 `.env` 配置中的 `RESEND_API_KEY` 和 `RESEND_FROM_EMAIL`
|
- 修改 `.env` 配置中的 `RESEND_API_KEY` 和 `RESEND_FROM_EMAIL`
|
||||||
`RESEND_FROM_EMAIL`: **noreply@域名**
|
`RESEND_FROM_EMAIL`: **noreply@域名**
|
||||||
`RESEND_API_KEY`:**刚刚复制的 Key**
|
`RESEND_API_KEY`:**刚刚复制的 Key**
|
||||||

|

|
||||||
|
|
||||||
## API文档
|
## API文档
|
||||||
|
|
||||||
### OpenAPI文档
|
### OpenAPI文档
|
||||||
|
|
||||||
https://docs.open-isle.com
|
https://docs.open-isle.com
|
||||||
|
|
||||||
### 部署时间线以及文档时效性
|
### 部署时间线以及文档时效性
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import io.swagger.v3.oas.annotations.media.Content;
|
|||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -131,6 +132,7 @@ public class CommentController {
|
|||||||
c.getId(),
|
c.getId(),
|
||||||
"comment",
|
"comment",
|
||||||
c.getCreatedAt(),
|
c.getCreatedAt(),
|
||||||
|
c.getPinnedAt(),
|
||||||
c // payload 是 CommentDto
|
c // payload 是 CommentDto
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -145,17 +147,39 @@ public class CommentController {
|
|||||||
l.getId(),
|
l.getId(),
|
||||||
"log",
|
"log",
|
||||||
l.getTime(), // 注意字段名不一样
|
l.getTime(), // 注意字段名不一样
|
||||||
|
null,
|
||||||
l // payload 是 PostChangeLogDto
|
l // payload 是 PostChangeLogDto
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.toList()
|
.toList()
|
||||||
);
|
);
|
||||||
// 排序
|
// 排序
|
||||||
Comparator<TimelineItemDto<?>> comparator = Comparator.comparing(TimelineItemDto::getCreatedAt);
|
Comparator<TimelineItemDto<?>> pinnedOrderComparator = (a, b) -> {
|
||||||
|
LocalDateTime aPinned = a.getPinnedAt();
|
||||||
|
LocalDateTime bPinned = b.getPinnedAt();
|
||||||
|
if (aPinned == null && bPinned == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (aPinned == null) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (bPinned == null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return bPinned.compareTo(aPinned);
|
||||||
|
};
|
||||||
|
|
||||||
|
Comparator<TimelineItemDto<?>> comparator = Comparator.<TimelineItemDto<?>, Boolean>comparing(
|
||||||
|
item -> item.getPinnedAt() == null
|
||||||
|
).thenComparing(pinnedOrderComparator);
|
||||||
|
|
||||||
|
Comparator<TimelineItemDto<?>> createdAtComparator = Comparator.comparing(
|
||||||
|
TimelineItemDto::getCreatedAt
|
||||||
|
);
|
||||||
if (CommentSort.NEWEST.equals(sort)) {
|
if (CommentSort.NEWEST.equals(sort)) {
|
||||||
comparator = comparator.reversed();
|
createdAtComparator = createdAtComparator.reversed();
|
||||||
}
|
}
|
||||||
itemDtoList.sort(comparator);
|
itemDtoList.sort(comparator.thenComparing(createdAtComparator));
|
||||||
log.debug("listComments returning {} comments", itemDtoList.size());
|
log.debug("listComments returning {} comments", itemDtoList.size());
|
||||||
return itemDtoList;
|
return itemDtoList;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,5 +15,6 @@ public class TimelineItemDto<T> {
|
|||||||
private Long id;
|
private Long id;
|
||||||
private String kind; // "comment" | "log"
|
private String kind; // "comment" | "log"
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime pinnedAt;
|
||||||
private T payload; // 泛型,具体类型由外部决定
|
private T payload; // 泛型,具体类型由外部决定
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -226,6 +226,8 @@ services:
|
|||||||
websocket-service:
|
websocket-service:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
profiles: ["staging", "prod"]
|
||||||
|
|
||||||
|
|
||||||
loopback_8080:
|
loopback_8080:
|
||||||
image: alpine/socat
|
image: alpine/socat
|
||||||
|
|||||||
@@ -341,6 +341,16 @@ body {
|
|||||||
.info-content-text pre {
|
.info-content-text pre {
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*处理iframe视频标签*/
|
||||||
|
.info-content-text iframe {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
aspect-ratio: 16 / 9; /* 保持 16:9 比例 */
|
||||||
|
border: none;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.d2h-file-name {
|
.d2h-file-name {
|
||||||
font-size: 14px !important;
|
font-size: 14px !important;
|
||||||
|
|||||||
@@ -49,7 +49,11 @@
|
|||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="open && !isMobile && (loading || filteredOptions.length > 0 || showSearch)"
|
v-if="
|
||||||
|
open &&
|
||||||
|
!isMobile &&
|
||||||
|
(loading || filteredOptions.length > 0 || showSearch || (remote && search))
|
||||||
|
"
|
||||||
:class="['dropdown-menu', menuClass]"
|
:class="['dropdown-menu', menuClass]"
|
||||||
v-click-outside="close"
|
v-click-outside="close"
|
||||||
ref="menuRef"
|
ref="menuRef"
|
||||||
@@ -62,26 +66,29 @@
|
|||||||
<l-hatch size="20" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
|
<l-hatch size="20" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div
|
<div v-if="filteredOptions.length === 0" class="dropdown-empty">没有搜索结果</div>
|
||||||
v-for="o in filteredOptions"
|
<template v-else>
|
||||||
:key="o.id"
|
<div
|
||||||
@click="select(o.id)"
|
v-for="o in filteredOptions"
|
||||||
:class="['dropdown-option', optionClass, { selected: isSelected(o.id) }]"
|
:key="o.id"
|
||||||
>
|
@click="select(o.id)"
|
||||||
<slot name="option" :option="o" :isSelected="isSelected(o.id)">
|
:class="['dropdown-option', optionClass, { selected: isSelected(o.id) }]"
|
||||||
<template v-if="o.icon">
|
>
|
||||||
<BaseImage
|
<slot name="option" :option="o" :isSelected="isSelected(o.id)">
|
||||||
v-if="isImageIcon(o.icon)"
|
<template v-if="o.icon">
|
||||||
:src="o.icon"
|
<BaseImage
|
||||||
class="option-icon"
|
v-if="isImageIcon(o.icon)"
|
||||||
:alt="o.name"
|
:src="o.icon"
|
||||||
/>
|
class="option-icon"
|
||||||
<component v-else :is="o.icon" class="option-icon" :size="16" />
|
:alt="o.name"
|
||||||
</template>
|
/>
|
||||||
<span>{{ o.name }}</span>
|
<component v-else :is="o.icon" class="option-icon" :size="16" />
|
||||||
</slot>
|
</template>
|
||||||
</div>
|
<span>{{ o.name }}</span>
|
||||||
<slot name="footer" :close="close" :loading="loading" />
|
</slot>
|
||||||
|
</div>
|
||||||
|
<slot name="footer" :close="close" :loading="loading" />
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
@@ -99,26 +106,29 @@
|
|||||||
<l-hatch size="20" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
|
<l-hatch size="20" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div
|
<div v-if="filteredOptions.length === 0" class="dropdown-empty">没有搜索结果</div>
|
||||||
v-for="o in filteredOptions"
|
<template v-else>
|
||||||
:key="o.id"
|
<div
|
||||||
@click="select(o.id)"
|
v-for="o in filteredOptions"
|
||||||
:class="['dropdown-option', optionClass, { selected: isSelected(o.id) }]"
|
:key="o.id"
|
||||||
>
|
@click="select(o.id)"
|
||||||
<slot name="option" :option="o" :isSelected="isSelected(o.id)">
|
:class="['dropdown-option', optionClass, { selected: isSelected(o.id) }]"
|
||||||
<template v-if="o.icon">
|
>
|
||||||
<BaseImage
|
<slot name="option" :option="o" :isSelected="isSelected(o.id)">
|
||||||
v-if="isImageIcon(o.icon)"
|
<template v-if="o.icon">
|
||||||
:src="o.icon"
|
<BaseImage
|
||||||
class="option-icon"
|
v-if="isImageIcon(o.icon)"
|
||||||
:alt="o.name"
|
:src="o.icon"
|
||||||
/>
|
class="option-icon"
|
||||||
<component v-else :is="o.icon" class="option-icon" :size="16" />
|
:alt="o.name"
|
||||||
</template>
|
/>
|
||||||
<span>{{ o.name }}</span>
|
<component v-else :is="o.icon" class="option-icon" :size="16" />
|
||||||
</slot>
|
</template>
|
||||||
</div>
|
<span>{{ o.name }}</span>
|
||||||
<slot name="footer" :close="close" :loading="loading" />
|
</slot>
|
||||||
|
</div>
|
||||||
|
<slot name="footer" :close="close" :loading="loading" />
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -283,6 +293,7 @@ export default {
|
|||||||
isImageIcon,
|
isImageIcon,
|
||||||
setSearch,
|
setSearch,
|
||||||
isMobile,
|
isMobile,
|
||||||
|
remote: props.remote,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -384,6 +395,13 @@ export default {
|
|||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdown-empty {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--muted-text-color, #8c8c8c);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.dropdown-mobile-page {
|
.dropdown-mobile-page {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|||||||
@@ -366,7 +366,11 @@ const changeLogIcon = (l) => {
|
|||||||
return 'unlock'
|
return 'unlock'
|
||||||
}
|
}
|
||||||
} else if (l.type === 'PINNED') {
|
} else if (l.type === 'PINNED') {
|
||||||
return 'pin-icon'
|
if(l.newPinnedAt){
|
||||||
|
return 'pin'
|
||||||
|
}else{
|
||||||
|
return 'clear-icon'
|
||||||
|
}
|
||||||
} else if (l.type === 'FEATURED') {
|
} else if (l.type === 'FEATURED') {
|
||||||
if (l.newFeatured) {
|
if (l.newFeatured) {
|
||||||
return 'star'
|
return 'star'
|
||||||
|
|||||||
@@ -849,7 +849,8 @@ watch(selectedTab, async (val) => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
gap: 20px;
|
row-gap: 40px; /* 行间距 */
|
||||||
|
column-gap: 20px; /* 列间距 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-title {
|
.summary-title {
|
||||||
@@ -888,10 +889,10 @@ watch(selectedTab, async (val) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.summary-divider {
|
.summary-divider {
|
||||||
margin-top: 20px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: 20px;
|
row-gap: 40px; /* 行间距 */
|
||||||
|
column-gap: 20px; /* 列间距 */
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,51 +18,6 @@ server {
|
|||||||
add_header X-Upstream $upstream_addr always;
|
add_header X-Upstream $upstream_addr always;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ^~ /api/ws {
|
|
||||||
proxy_pass http://127.0.0.1:8080;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
|
|
||||||
# 升级所需
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection $connection_upgrade;
|
|
||||||
|
|
||||||
# 统一透传这些头(你在 /api/ 有,/api/ws 也要有)
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_set_header X-Forwarded-Host $host;
|
|
||||||
|
|
||||||
proxy_read_timeout 300s;
|
|
||||||
proxy_send_timeout 300s;
|
|
||||||
proxy_buffering off;
|
|
||||||
proxy_cache off;
|
|
||||||
}
|
|
||||||
|
|
||||||
# 2) SockJS(包含 /info、/iframe.html、/.../websocket 等)
|
|
||||||
location ^~ /api/sockjs {
|
|
||||||
proxy_pass http://127.0.0.1:8080;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection $connection_upgrade;
|
|
||||||
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_set_header X-Forwarded-Host $host;
|
|
||||||
|
|
||||||
proxy_read_timeout 300s;
|
|
||||||
proxy_send_timeout 300s;
|
|
||||||
proxy_buffering off;
|
|
||||||
proxy_cache off;
|
|
||||||
|
|
||||||
# 如要同源 iframe 回退,下面两行二选一(或者交给 Spring Security 的 sameOrigin)
|
|
||||||
# proxy_hide_header X-Frame-Options;
|
|
||||||
# add_header X-Frame-Options "SAMEORIGIN" always;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /api/ {
|
location /api/ {
|
||||||
proxy_pass http://127.0.0.1:8080/api/;
|
proxy_pass http://127.0.0.1:8080/api/;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
@@ -148,7 +103,7 @@ server {
|
|||||||
|
|
||||||
# ---------- WEBSOCKET GATEWAY TO :8082 ----------
|
# ---------- WEBSOCKET GATEWAY TO :8082 ----------
|
||||||
location ^~ /websocket/ {
|
location ^~ /websocket/ {
|
||||||
proxy_pass http://127.0.0.1:8082/;
|
proxy_pass http://127.0.0.1:8084/;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
@@ -167,6 +122,7 @@ server {
|
|||||||
add_header Cache-Control "no-store" always;
|
add_header Cache-Control "no-store" always;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name open-isle.com www.open-isle.com;
|
server_name open-isle.com www.open-isle.com;
|
||||||
|
|||||||
Reference in New Issue
Block a user