mirror of
https://github.com/ZeroCatDev/ClassworksKV.git
synced 2025-10-21 17:53:11 +00:00
cskv
This commit is contained in:
parent
7b1e224f70
commit
aec482cbcb
@ -38,18 +38,12 @@ const error = ref(null);
|
||||
const showDialog = ref(false);
|
||||
|
||||
// 从环境变量获取 assets URL
|
||||
const assetsBaseUrl = import.meta.env.VITE_ASSETS_URL || "";
|
||||
const assetsBaseUrl = "https://zerocat-bitiful.houlangs.com/material/asset";
|
||||
|
||||
// 根据 iconHash 生成图片 URL
|
||||
// 根据 logo_url 生成图片 URL
|
||||
const iconUrl = computed(() => {
|
||||
if (!app.value?.iconHash) return null;
|
||||
const hash = app.value.iconHash;
|
||||
if (hash.length < 4) return null;
|
||||
|
||||
const folder1 = hash.substring(0, 2);
|
||||
const folder2 = hash.substring(2, 4);
|
||||
|
||||
return `${assetsBaseUrl}/${folder1}/${folder2}/${hash}.webp`;
|
||||
if (!app.value?.logo_url) return null;
|
||||
return `${assetsBaseUrl}/${app.value.logo_url}`;
|
||||
});
|
||||
|
||||
// 渲染 Markdown 为 HTML
|
||||
@ -61,9 +55,15 @@ const renderedReadme = computed(() => {
|
||||
// 获取应用信息
|
||||
const fetchApp = async () => {
|
||||
try {
|
||||
app.value = await axios.get(`/apps/info/${props.appId}`);
|
||||
const response = await fetch(`https://zerocat-api.houlangs.com/oauth/applications/${props.appId}`);
|
||||
|
||||
if (app.value.repositoryUrl) {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch app info: ${response.status}`);
|
||||
}
|
||||
|
||||
app.value = await response.json();
|
||||
|
||||
if (app.value.homepage_url) {
|
||||
await fetchReadme();
|
||||
}
|
||||
} catch (err) {
|
||||
@ -75,9 +75,9 @@ const fetchApp = async () => {
|
||||
|
||||
// 检测 Git 平台并获取 README
|
||||
const fetchReadme = async () => {
|
||||
if (!app.value?.repositoryUrl) return;
|
||||
if (!app.value?.homepage_url) return;
|
||||
|
||||
const url = app.value.repositoryUrl;
|
||||
const url = app.value.homepage_url;
|
||||
let readmeUrl = null;
|
||||
|
||||
try {
|
||||
@ -207,7 +207,7 @@ fetchApp();
|
||||
{{ app.description }}
|
||||
</CardDescription>
|
||||
<div class="mt-2 text-xs text-muted-foreground">
|
||||
<span>{{ app.developerName }}</span>
|
||||
<span>{{ app.owner?.display_name || app.owner?.username }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -239,23 +239,12 @@ fetchApp();
|
||||
<div class="grid grid-cols-2 gap-4 py-4 border-y">
|
||||
<div class="space-y-1">
|
||||
<div class="text-sm text-muted-foreground">开发者</div>
|
||||
<div class="font-medium">{{ app.developerName }}</div>
|
||||
<div class="font-medium">{{ app.owner?.display_name || app.owner?.username }}</div>
|
||||
</div>
|
||||
<div v-if="app.developerLink" class="space-y-1">
|
||||
<div class="text-sm text-muted-foreground">开发者链接</div>
|
||||
<a
|
||||
:href="app.developerLink"
|
||||
target="_blank"
|
||||
class="text-primary hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
访问
|
||||
<ExternalLink class="h-3 w-3" />
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="app.homepageLink" class="space-y-1">
|
||||
<div v-if="app.homepage_url" class="space-y-1">
|
||||
<div class="text-sm text-muted-foreground">应用主页</div>
|
||||
<a
|
||||
:href="app.homepageLink"
|
||||
:href="app.homepage_url"
|
||||
target="_blank"
|
||||
class="text-primary hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
@ -263,14 +252,25 @@ fetchApp();
|
||||
<ExternalLink class="h-3 w-3" />
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="app.repositoryUrl" class="space-y-1">
|
||||
<div class="text-sm text-muted-foreground">仓库地址</div>
|
||||
<div v-if="app.terms_url" class="space-y-1">
|
||||
<div class="text-sm text-muted-foreground">服务条款</div>
|
||||
<a
|
||||
:href="app.repositoryUrl"
|
||||
:href="app.terms_url"
|
||||
target="_blank"
|
||||
class="text-primary hover:underline inline-flex items-center gap-1 truncate"
|
||||
>
|
||||
查看仓库
|
||||
查看
|
||||
<ExternalLink class="h-3 w-3" />
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="app.privacy_url" class="space-y-1">
|
||||
<div class="text-sm text-muted-foreground">隐私政策</div>
|
||||
<a
|
||||
:href="app.privacy_url"
|
||||
target="_blank"
|
||||
class="text-primary hover:underline inline-flex items-center gap-1 truncate"
|
||||
>
|
||||
查看
|
||||
<ExternalLink class="h-3 w-3" />
|
||||
</a>
|
||||
</div>
|
||||
@ -286,7 +286,7 @@ fetchApp();
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="!loading && app?.repositoryUrl"
|
||||
v-else-if="!loading && app?.homepage_url"
|
||||
class="mt-6 text-center text-muted-foreground"
|
||||
>
|
||||
无法加载 README 文件
|
||||
|
@ -13,6 +13,7 @@ import { CheckCircle2, XCircle, Loader2, Shield, Key, AlertCircle, User, Plus, C
|
||||
import AppCard from '@/components/AppCard.vue'
|
||||
import PasswordInput from '@/components/PasswordInput.vue'
|
||||
import LoginDialog from '@/components/LoginDialog.vue'
|
||||
import DeviceRegisterDialog from '@/components/DeviceRegisterDialog.vue'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
const route = useRoute()
|
||||
@ -34,6 +35,8 @@ const deviceAccount = ref(null)
|
||||
const showLoginDialog = ref(false)
|
||||
const showDeviceList = ref(false)
|
||||
const customDeviceUuid = ref('')
|
||||
const showRegisterDialog = ref(false)
|
||||
const deviceRequired = ref(false)
|
||||
|
||||
// 计算属性获取是否有密码
|
||||
const hasPassword = computed(() => deviceInfo.value?.hasPassword || false)
|
||||
@ -224,14 +227,30 @@ const loadDeviceInfo = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 更新设备UUID回调
|
||||
const updateUuid = () => {
|
||||
showRegisterDialog.value = false
|
||||
deviceUuid.value = deviceStore.getDeviceUuid()
|
||||
loadDeviceInfo()
|
||||
loadDeviceAccount()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
deviceUuid.value = deviceStore.getOrGenerate()
|
||||
// 检查是否存在设备UUID
|
||||
const existingUuid = deviceStore.getDeviceUuid()
|
||||
if (!existingUuid) {
|
||||
deviceRequired.value = true
|
||||
// 如果没有设备UUID,显示设备管理弹框
|
||||
showRegisterDialog.value = true
|
||||
} else {
|
||||
deviceUuid.value = existingUuid
|
||||
|
||||
// 加载设备信息
|
||||
await loadDeviceInfo()
|
||||
// 加载设备信息
|
||||
await loadDeviceInfo()
|
||||
|
||||
// 加载设备账户信息
|
||||
await loadDeviceAccount()
|
||||
// 加载设备账户信息
|
||||
await loadDeviceAccount()
|
||||
}
|
||||
|
||||
// 加载应用信息
|
||||
await loadAppInfo()
|
||||
@ -261,20 +280,16 @@ onMounted(async () => {
|
||||
<div class="space-y-2 text-center">
|
||||
<CardTitle class="text-2xl">应用授权</CardTitle>
|
||||
<CardDescription>
|
||||
<template v-if="appInfo">
|
||||
授权 <span class="font-semibold">{{ appInfo.name }}</span> 访问您的 KV 存储
|
||||
</template>
|
||||
<template v-else>
|
||||
|
||||
授权应用访问您的 KV 存储
|
||||
</template>
|
||||
</CardDescription>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent class="space-y-6">
|
||||
<!-- 应用信息 -->
|
||||
<div v-if="appInfo">
|
||||
<AppCard :app-id="parseInt(appId)" class="mb-4" />
|
||||
<div>
|
||||
<AppCard :app-id="appId" class="mb-4" />
|
||||
</div>
|
||||
|
||||
<!-- 设备信息 -->
|
||||
@ -467,5 +482,12 @@ onMounted(async () => {
|
||||
|
||||
<!-- 登录弹框 -->
|
||||
<LoginDialog v-model="showLoginDialog" :on-success="handleLoginSuccess" />
|
||||
|
||||
<!-- 设备注册弹框 -->
|
||||
<DeviceRegisterDialog
|
||||
v-model="showRegisterDialog"
|
||||
@confirm="updateUuid"
|
||||
:required="deviceRequired"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -27,6 +27,7 @@ const copied = ref(null)
|
||||
const deviceInfo = ref(null) // 存储设备信息
|
||||
const deviceAccount = ref(null) // 设备账户信息
|
||||
const accountStore = useAccountStore()
|
||||
const appInfoCache = ref({}) // 缓存应用信息
|
||||
|
||||
// Dialogs
|
||||
const showAuthorizeDialog = ref(false)
|
||||
@ -63,8 +64,6 @@ const groupedByApp = computed(() => {
|
||||
if (!groups[appId]) {
|
||||
groups[appId] = {
|
||||
appId: appId,
|
||||
appName: token.appName || appId,
|
||||
description: token.appDescription || '',
|
||||
tokens: []
|
||||
}
|
||||
}
|
||||
@ -108,6 +107,20 @@ const loadTokens = async () => {
|
||||
try {
|
||||
const response = await apiClient.getDeviceTokens(deviceUuid.value)
|
||||
tokens.value = response.tokens || []
|
||||
|
||||
// 预加载应用信息
|
||||
for (const token of tokens.value) {
|
||||
if (!appInfoCache.value[token.appId]) {
|
||||
try {
|
||||
const appResponse = await fetch(`https://zerocat-api.houlangs.com/oauth/applications/${token.appId}`)
|
||||
if (appResponse.ok) {
|
||||
appInfoCache.value[token.appId] = await appResponse.json()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to load app info for ${token.appId}:`, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load tokens:', error)
|
||||
if (error.message.includes('设备不存在')) {
|
||||
@ -118,6 +131,11 @@ const loadTokens = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取应用名称的辅助函数
|
||||
const getAppName = (appId) => {
|
||||
return appInfoCache.value[appId]?.name || `应用 ${appId}`
|
||||
}
|
||||
|
||||
const authorizeApp = async () => {
|
||||
if (!appIdToAuthorize.value) return
|
||||
|
||||
@ -715,7 +733,7 @@ onMounted(async () => {
|
||||
<div class="p-4 bg-muted rounded-lg space-y-2">
|
||||
<div class="text-sm">
|
||||
<span class="font-medium">应用: </span>
|
||||
{{ selectedToken.appName }}
|
||||
{{ getAppName(selectedToken.appId) }}
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<span class="font-medium">令牌: </span>
|
||||
|
@ -42,6 +42,8 @@ const showChangePasswordDialog = ref(false)
|
||||
const showDeletePasswordDialog = ref(false)
|
||||
const showHintDialog = ref(false)
|
||||
const showResetDeviceDialog = ref(false)
|
||||
const showRegisterDialog = ref(false)
|
||||
const deviceRequired = ref(false)
|
||||
|
||||
// Form data
|
||||
const currentPassword = ref('')
|
||||
@ -237,15 +239,30 @@ const handleDeviceReset = () => {
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
// 更新设备UUID回调
|
||||
const updateUuid = () => {
|
||||
showRegisterDialog.value = false
|
||||
deviceUuid.value = deviceStore.getDeviceUuid()
|
||||
loadDeviceInfo()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
deviceUuid.value = deviceStore.getOrGenerate()
|
||||
// 检查是否存在设备UUID
|
||||
const existingUuid = deviceStore.getDeviceUuid()
|
||||
if (!existingUuid) {
|
||||
deviceRequired.value = true
|
||||
// 如果没有设备UUID,显示设备管理弹框
|
||||
showRegisterDialog.value = true
|
||||
} else {
|
||||
deviceUuid.value = existingUuid
|
||||
|
||||
// 加载设备信息
|
||||
await loadDeviceInfo()
|
||||
// 加载设备信息
|
||||
await loadDeviceInfo()
|
||||
|
||||
// 如果有密码但密码提示不存在,单独加载密码提示
|
||||
if (hasPassword.value && !passwordHint.value) {
|
||||
await loadPasswordHint()
|
||||
// 如果有密码但密码提示不存在,单独加载密码提示
|
||||
if (hasPassword.value && !passwordHint.value) {
|
||||
await loadPasswordHint()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@ -587,5 +604,12 @@ onMounted(async () => {
|
||||
@confirm="handleDeviceReset"
|
||||
@update:modelValue="val => showResetDeviceDialog = val"
|
||||
/>
|
||||
|
||||
<!-- 必需注册弹框 -->
|
||||
<DeviceRegisterDialog
|
||||
v-model="showRegisterDialog"
|
||||
@confirm="updateUuid"
|
||||
:required="deviceRequired"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
@ -27,7 +27,6 @@ export const kvTokenAuth = async (req, res, next) => {
|
||||
const appInstall = await prisma.appInstall.findUnique({
|
||||
where: { token },
|
||||
include: {
|
||||
app: true,
|
||||
device: true,
|
||||
},
|
||||
});
|
||||
@ -38,7 +37,6 @@ export const kvTokenAuth = async (req, res, next) => {
|
||||
|
||||
// 将信息存储到res.locals供后续使用
|
||||
res.locals.device = appInstall.device;
|
||||
res.locals.app = appInstall.app;
|
||||
res.locals.appInstall = appInstall;
|
||||
res.locals.deviceId = appInstall.device.id;
|
||||
|
||||
|
17
prisma/migrations/20251005011323_update/migration.sql
Normal file
17
prisma/migrations/20251005011323_update/migration.sql
Normal file
@ -0,0 +1,17 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `App` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE `AppInstall` DROP FOREIGN KEY `AppInstall_appId_fkey`;
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX `AppInstall_appId_fkey` ON `AppInstall`;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `AppInstall` MODIFY `appId` VARCHAR(191) NOT NULL;
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE `App`;
|
@ -56,27 +56,10 @@ model Device {
|
||||
kvStore KVStore[] // 设备相关的KV存储
|
||||
}
|
||||
|
||||
model App {
|
||||
id Int @id @default(autoincrement()) // 自增ID
|
||||
name String // 应用名称
|
||||
description String? // 应用简介
|
||||
developerName String // 开发者名称
|
||||
developerLink String? // 开发者链接
|
||||
homepageLink String? // 应用首页链接
|
||||
iconHash String? // 图标hash
|
||||
repositoryUrl String? // Git仓库地址 (支持 GitHub, GitLab, Bitbucket, Gitea, Forgejo)
|
||||
metadata Json? // 元数据
|
||||
createdAt DateTime @default(now()) // 自动生成创建时间
|
||||
updatedAt DateTime @updatedAt // 自动更新时间
|
||||
|
||||
// 关联的应用安装记录
|
||||
installs AppInstall[]
|
||||
}
|
||||
|
||||
model AppInstall {
|
||||
id String @id @default(cuid())
|
||||
deviceId Int // 关联的设备ID
|
||||
appId Int // 关联的应用ID
|
||||
appId String // 应用ID (SHA256 hash)
|
||||
token String @unique // 应用安装的唯一访问令牌,拥有完整KV读写权限
|
||||
note String? // 安装备注
|
||||
installedAt DateTime @default(now())
|
||||
@ -84,5 +67,4 @@ model AppInstall {
|
||||
|
||||
// 关联关系
|
||||
device Device @relation(fields: [deviceId], references: [id], onDelete: Cascade)
|
||||
app App @relation(fields: [appId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
@ -465,17 +465,6 @@ router.get("/devices", jwtAuth, async (req, res, next) => {
|
||||
name: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
appInstalls: {
|
||||
include: {
|
||||
app: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
iconHash: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -28,14 +28,12 @@ router.get(
|
||||
|
||||
const installations = await prisma.appInstall.findMany({
|
||||
where: { deviceId: device.id },
|
||||
include: { app: true },
|
||||
});
|
||||
|
||||
const apps = installations.map(install => ({
|
||||
id: install.app.id,
|
||||
name: install.app.name,
|
||||
description: install.app.description,
|
||||
appId: install.appId,
|
||||
token: install.token,
|
||||
note: install.note,
|
||||
installedAt: install.createdAt,
|
||||
}));
|
||||
|
||||
@ -49,6 +47,7 @@ router.get(
|
||||
/**
|
||||
* POST /apps/devices/:uuid/install/:appId
|
||||
* 为设备安装应用 (需要UUID认证)
|
||||
* appId 现在是 SHA256 hash
|
||||
*/
|
||||
router.post(
|
||||
"/devices/:uuid/install/:appId",
|
||||
@ -58,16 +57,6 @@ router.post(
|
||||
const { appId } = req.params;
|
||||
const { note } = req.body;
|
||||
|
||||
// 检查应用是否存在
|
||||
const app = await prisma.app.findUnique({
|
||||
where: { id: parseInt(appId) },
|
||||
});
|
||||
|
||||
if (!app) {
|
||||
return next(errors.createError(404, "应用不存在"));
|
||||
}
|
||||
|
||||
|
||||
// 生成token
|
||||
const token = crypto.randomBytes(32).toString("hex");
|
||||
|
||||
@ -75,7 +64,7 @@ router.post(
|
||||
const installation = await prisma.appInstall.create({
|
||||
data: {
|
||||
deviceId: device.id,
|
||||
appId: app.id,
|
||||
appId: appId,
|
||||
token,
|
||||
note: note || null,
|
||||
},
|
||||
@ -83,8 +72,7 @@ router.post(
|
||||
|
||||
return res.status(201).json({
|
||||
id: installation.id,
|
||||
appId: app.id,
|
||||
appName: app.name,
|
||||
appId: installation.appId,
|
||||
token: installation.token,
|
||||
note: installation.note,
|
||||
installedAt: installation.createdAt,
|
||||
@ -124,30 +112,6 @@ router.delete(
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* GET /apps
|
||||
* 获取所有可用应用列表
|
||||
*/
|
||||
router.get(
|
||||
"/",
|
||||
errors.catchAsync(async (req, res) => {
|
||||
const apps = await prisma.app.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
description: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
apps,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* GET /apps/tokens
|
||||
* 获取设备的token列表 (需要设备UUID)
|
||||
@ -173,24 +137,13 @@ router.get(
|
||||
// 获取该设备的所有应用安装记录(即token)
|
||||
const installations = await prisma.appInstall.findMany({
|
||||
where: { deviceId: device.id },
|
||||
include: {
|
||||
app: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
description: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { installedAt: 'desc' },
|
||||
});
|
||||
|
||||
const tokens = installations.map(install => ({
|
||||
id: install.id, // 安装记录ID
|
||||
id: install.id,
|
||||
token: install.token,
|
||||
appId: install.app.id,
|
||||
appName: install.app.name,
|
||||
appDescription: install.app.description,
|
||||
appId: install.appId,
|
||||
installedAt: install.installedAt,
|
||||
note: install.note,
|
||||
}));
|
||||
@ -202,16 +155,5 @@ router.get(
|
||||
});
|
||||
})
|
||||
);
|
||||
router.get("/info/:appid",
|
||||
errors.catchAsync(async (req, res, next) => {
|
||||
const { appid } = req.params;
|
||||
const app = await prisma.app.findUnique({
|
||||
where: { id: parseInt(appid) },
|
||||
});
|
||||
if (!app) {
|
||||
return next(errors.createError(404, "应用不存在"));
|
||||
}
|
||||
return res.json(app);
|
||||
})
|
||||
);
|
||||
|
||||
export default router;
|
@ -3,10 +3,58 @@ const router = Router();
|
||||
import kvStore from "../utils/kvStore.js";
|
||||
import { kvTokenAuth } from "../middleware/kvTokenAuth.js";
|
||||
import errors from "../utils/errors.js";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// 使用KV专用token认证
|
||||
router.use(kvTokenAuth);
|
||||
|
||||
/**
|
||||
* GET /_info
|
||||
* 获取当前token所属设备的信息,如果关联了账号也返回账号信息
|
||||
*/
|
||||
router.get(
|
||||
"/_info",
|
||||
errors.catchAsync(async (req, res) => {
|
||||
const deviceId = res.locals.deviceId;
|
||||
|
||||
// 获取设备信息,包含关联的账号
|
||||
const device = await prisma.device.findUnique({
|
||||
where: { id: deviceId },
|
||||
include: {
|
||||
account: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!device) {
|
||||
return next(errors.createError(404, "设备不存在"));
|
||||
}
|
||||
|
||||
// 构建响应对象
|
||||
const response = {
|
||||
device: {
|
||||
id: device.id,
|
||||
uuid: device.uuid,
|
||||
name: device.name,
|
||||
createdAt: device.createdAt,
|
||||
updatedAt: device.updatedAt,
|
||||
},
|
||||
};
|
||||
|
||||
// 如果关联了账号,添加账号信息
|
||||
if (device.account) {
|
||||
response.account = {
|
||||
id: device.account.id,
|
||||
name: device.account.name,
|
||||
avatarUrl: device.account.avatarUrl,
|
||||
};
|
||||
}
|
||||
|
||||
return res.json(response);
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* GET /_keys
|
||||
* 获取当前token对应设备的键名列表(分页,不包括内容)
|
||||
|
Loading…
x
Reference in New Issue
Block a user