Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat:新增对 AList API 的支持 #10

Merged
merged 7 commits into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .env.local
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ Bucket=""
STORAGE_FOLDER="kamera"
# CDN 域名,请严格按照 example.com 格式,不需要添加 https:// 会自动补充
CDN_URL="kamera-s3-cdn.heming.dev"
# 存储模式:本地维护填 local / 对象存储填 s3。选本地的话,就会从 assets/server 下加载 json 文件,你需要手动维护;选对象存储,通过后台功能进行维护。
STORAGE_MODEL="s3"
# alist 地址
ALIST_URL="https://alist.besscroft.com"
# alist 令牌
ALIST_TOKEN="alist-***"
# 系统用户 账号
KAMERA_USERNAME="admin"
# 系统用户 密码
Expand Down
6 changes: 4 additions & 2 deletions .env.production
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ Bucket=""
STORAGE_FOLDER="kamera"
# CDN 域名,请严格按照 example.com 格式,不需要添加 https:// 会自动补充
CDN_URL=""
# 存储模式:本地维护填 local / 对象存储填 s3。选本地的话,就会从 assets/server 下加载 json 文件,你需要手动维护;选对象存储,通过后台功能进行维护。
STORAGE_MODEL="s3"
# alist 地址
ALIST_URL="https://alist.besscroft.com"
# alist 令牌
ALIST_TOKEN="alist-***"
# 系统用户 账号
KAMERA_USERNAME="admin"
# 系统用户 密码
Expand Down
45 changes: 25 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ nitro: {

> 请确保您的数据库用户配置了正确的 Row Level Security(行级别安全性)权限,否则将无法正常访问。

#### 图片存储

从 v0.0.6 版本开始,重构了图片的上传逻辑,将同时支持兼容 S3 的存储,以及 AList,便于用户更加灵活的选择!

存储你可以选择 AWS S3、阿里云 OSS 或者自建 MinIO,也可以交由你正在用的 AList 来维护。

#### 环境变量

请在部署前设置您的环境变量,程序会去读这些值,用以更改构建步骤或函数执行期间的行为。
Expand All @@ -50,26 +56,25 @@ nitro: {
>
> 项目内默认的 key 都是用作演示用途!

| Key | 备注 |
| ---------------- |-------------------------------------------------------------------------------------------------------------|
| Postgre_HOST | Postgre 数据库主机,如:db.kamera.supabase.co |
| Postgre_PORT | Postgre 数据库端口,默认值:5432 |
| Postgre_DATABASE | Postgre 数据库名称,默认值:postgres |
| Postgre_USERNAME | Postgre 数据库用户名,默认值:postgres |
| Postgre_PASSWORD | Postgre 数据库密码,默认值:postgres |
| AUTH_KEY | 权限 key,jwt 和 hash 都需要用到它。如果您更改了它,请自行生成新密码,默认值:kamera |
| AccessKey_ID | 阿里 OSS / AWS S3 AccessKey_ID |
| AccessKey_Secret | 阿里 OSS / AWS S3 AccessKey_Secret |
| Region | 阿里 OSS / AWS S3 Region 地域,如:oss-cn-hongkong |
| Endpoint | 阿里 OSS / AWS S3 Endpoint 地域节点,如:oss-cn-hongkong.aliyuncs.com |
| Bucket | 阿里 OSS / AWS S3 Bucket 存储桶名称,如:kamera |
| STORAGE_FOLDER | 存储文件夹,严格格式,如:kamera 或 kamera/images ,填 / 或者不填表示根路径 |
| CDN_URL | CDN 域名,请严格按照 example.com 格式,不需要添加 https:// 会自动补充,如:`kamera-s3-cdn.heming.dev`,CDN 以兼容阿里云 OSS 为主,理论上适配大多数存储。 |
| STORAGE_MODEL | 存储模式:暂仅支持对象存储(包括自建的 MinIO),值 `s3`。默认值:`s3` |
| KAMERA_USERNAME | 系统用户账号,默认值:admin,单次登录有效期 24 小时。 |
| KAMERA_PASSWORD | 系统用户密码,默认值(666666)在 `.env.local` 文件中可查,如果需要更改密码,可在登录后进入后台自行生成后替换。 |

> 理论上存储兼容 AWS S3 API,除了阿里云 OSS 外,其它兼容 S3 API 的存储都可以使用。
| Key | 备注 |
| ---------------- |-------------------------------------------------------------------------------------------------------------------|
| Postgre_HOST | Postgre 数据库主机,如:db.kamera.supabase.co |
| Postgre_PORT | Postgre 数据库端口,默认值:5432 |
| Postgre_DATABASE | Postgre 数据库名称,默认值:postgres |
| Postgre_USERNAME | Postgre 数据库用户名,默认值:postgres |
| Postgre_PASSWORD | Postgre 数据库密码,默认值:postgres |
| AUTH_KEY | 权限 key,jwt 和 hash 都需要用到它。如果您更改了它,请自行生成新密码,默认值:kamera |
| AccessKey_ID | 阿里 OSS / AWS S3 AccessKey_ID |
| AccessKey_Secret | 阿里 OSS / AWS S3 AccessKey_Secret |
| Region | 阿里 OSS / AWS S3 Region 地域,如:oss-cn-hongkong |
| Endpoint | 阿里 OSS / AWS S3 Endpoint 地域节点,如:oss-cn-hongkong.aliyuncs.com |
| Bucket | 阿里 OSS / AWS S3 Bucket 存储桶名称,如:kamera |
| STORAGE_FOLDER | 存储文件夹(S3),严格格式,如:kamera 或 kamera/images ,填 / 或者不填表示根路径 |
| ALIST_URL | AList 地址,如:https://alist.besscroft.com |
| ALIST_TOKEN | alist 令牌 |
| CDN_URL | CDN 域名(仅 S3),请严格按照 example.com 格式,不需要添加 https:// 会自动补充,如:`kamera-s3-cdn.heming.dev`,CDN 以兼容阿里云 OSS 为主,理论上适配大多数存储。 |
| KAMERA_USERNAME | 系统用户账号,默认值:admin,单次登录有效期 24 小时。 |
| KAMERA_PASSWORD | 系统用户密码,默认值(666666)在 `.env.local` 文件中可查,如果需要更改密码,可在登录后进入后台自行生成后替换。 |

#### 页面配置

Expand Down
140 changes: 114 additions & 26 deletions pages/admin/index.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import photosList from '~/constants/photos.json'
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
import * as ExifReader from 'exifreader'
import photosList from '~/constants/photos.json'
import { useUserStore } from '~/composables/user'

const breakpoints = useBreakpoints(breakpointsTailwind)
Expand All @@ -11,33 +11,50 @@ const user = useUserStore()
const fileUrl = ref('')
const loading = ref<boolean>(false)
const toast = useToast()
const mountSelectShow = ref(false)

const storage = ref('')
const imgData = reactive({
mountPath: '',
type: '',
url: '',
exif: {},
detail: '',
rating: ''
rating: '',
})

const options = ref([
const storageOptions = ref([
{
label: 'S3',
value: 's3',
},
{
label: 'AList',
value: 'alist',
},
])
const mountOptions = ref([
])
const imgTypeOptions = ref([
{
label: '首页精选',
value: 'index',
},
])

/** 自定义上传请求 */
const onRequestUpload = async (option: any) => {
async function onRequestUpload(option: any) {
const file = option.file
let formData = new FormData();
formData.append('file', file);
formData.append('type', imgData.type || '');
const formData = new FormData()
formData.append('file', file)
formData.append('storage', storage.value || '')
formData.append('type', imgData.type || '')
formData.append('mountPath', imgData.mountPath || '')
const { data, url } = await $fetch('/api/uploadFile', {
timeout: 60000,
method: 'post',
headers: {
Authorization: `${user.tokenName} ${user.token}`
Authorization: `${user.tokenName} ${user.token}`,
},
body: formData,
})
Expand All @@ -62,9 +79,19 @@ const onRequestUpload = async (option: any) => {
}
}

const submit = async () => {
async function submit() {
loading.value = true
try {
if (storage.value === '') {
toast.add({ title: '请选择存储!', timeout: 2000, color: 'red' })
loading.value = false
return
}
if (storage.value === 'alist' && imgData.mountPath === '') {
toast.add({ title: '请选择挂载目录!', timeout: 2000, color: 'red' })
loading.value = false
return
}
if (imgData.type === '') {
toast.add({ title: '请选择类型!', timeout: 2000, color: 'red' })
loading.value = false
Expand All @@ -74,32 +101,43 @@ const submit = async () => {
timeout: 60000,
method: 'post',
headers: {
Authorization: `${user.tokenName} ${user.token}`
Authorization: `${user.tokenName} ${user.token}`,
},
body: imgData,
})
if (data === 0) {
toast.add({ title: '保存成功!', timeout: 2000 })
} else {
}
else {
toast.add({ title: '保存失败!', timeout: 2000, color: 'red' })
}
} catch (e) {
}
catch (e) {
loading.value = false
}
loading.value = false
}

const removeFile = () => {
fileUrl.value = '';
imgData.url = '';
imgData.rating = 0;
imgData.detail = '';
imgData.exif = {};
imgData.type = '';
function removeFile() {
mountOptions.value = []
fileUrl.value = ''
storage.value = ''
imgData.mountPath = ''
imgData.url = ''
imgData.rating = 0
imgData.detail = ''
imgData.exif = {}
imgData.type = ''
}

const onBeforeUpload = (file: any) => {
if (!imgData.type || imgData.type === '') {
function onBeforeUpload(file: any) {
if (!storage.value || storage.value === '') {
toast.add({ title: '请先选择存储!', timeout: 2000, color: 'red' })
file.abort()
} else if (storage.value === 'alist' && (!imgData.mountPath || imgData.mountPath === '')) {
toast.add({ title: '请先选择挂载目录!', timeout: 2000, color: 'red' })
file.abort()
} else if (!imgData.type || imgData.type === '') {
toast.add({ title: '请先选择图片类别!', timeout: 2000, color: 'red' })
file.abort()
} else {
Expand All @@ -111,17 +149,49 @@ const exceed = () => {
toast.add({ title: '只能同时上传一张图片!', timeout: 2000, color: 'red' })
}

watch(storage, async (val) => {
if (val === 'alist') {
if (mountOptions.value.length === 0) {
const { data } = await $fetch('/api/getStorageList', {
timeout: 60000,
method: 'get',
headers: {
Authorization: `${user.tokenName} ${user.token}`,
},
})
if (data) {
// 遍历数组,给 mountOptions 赋值
data.forEach((item: any) => {
if (item.status === 'work') {
mountOptions.value.push({
label: item.mount_path,
value: item.mount_path,
})
}
})
}
}
mountSelectShow.value = true
} else {
mountSelectShow.value = false
}
})

onBeforeMount(() => {
if (photosList) {
photosList?.forEach((photo: any) => {
options.value.push({
imgTypeOptions.value.push({
label: photo.title,
value: photo.url.replace('/', ''),
});
});
})
})
}
})

onUnmounted(() => {
removeFile()
})

definePageMeta({
layout: 'admin',
})
Expand All @@ -131,15 +201,33 @@ definePageMeta({
<div w-full max-h-full md:max-w-7xl flex flex-col items-center justify-center mx-auto p2 md:p8 pb-20>
<div my-6 md:my-16 mx-auto w-full md:max-w-4xl rounded-md bg-white dark:bg-gray-800 shadow p-2>
<div flex items-center justify-center pb-2 space-x-2>
<el-select v-model="storage" m-2 placeholder="请选择存储">
<el-option
v-for="item in storageOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-select v-model="imgData.type" m-2 placeholder="请选择图片类别">
<el-option
v-for="item in options"
v-for="item in imgTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-button round v-if="fileUrl" :loading="loading" @click="submit"> 保存 </el-button>
</div>
<div v-if="mountSelectShow && mountOptions.length > 0" flex items-center justify-center pb-2>
<el-select v-model="imgData.mountPath" m-2 placeholder="请选择挂载目录">
<el-option
v-for="item in mountOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-button round v-if="fileUrl" :loading="loading" @click="submit">保存</el-button>
</div>
<el-upload
class="upload-demo"
Expand Down
42 changes: 21 additions & 21 deletions plugins/chunk-reload.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,30 @@ import { joinURL } from 'ufo'
import type { RouteLocationNormalized } from 'vue-router'

export default defineNuxtPlugin({
name: 'nuxt:chunk-reload',
setup (nuxtApp) {
const router = useRouter()
const config = useRuntimeConfig()
name: 'nuxt:chunk-reload',
setup(nuxtApp) {
const router = useRouter()
const config = useRuntimeConfig()

const chunkErrors = new Set()
const chunkErrors = new Set()

router.beforeEach(() => { chunkErrors.clear() })
nuxtApp.hook('app:chunkError', ({ error }) => { chunkErrors.add(error) })
router.beforeEach(() => { chunkErrors.clear() })
nuxtApp.hook('app:chunkError', ({ error }) => { chunkErrors.add(error) })

function reloadAppAtPath (to: RouteLocationNormalized) {
const isHash = 'href' in to && (to.href as string).startsWith('#')
const path = isHash ? config.app.baseURL + (to as any).href : joinURL(config.app.baseURL, to.fullPath)
reloadNuxtApp({ path, persistState: true })
}
function reloadAppAtPath(to: RouteLocationNormalized) {
const isHash = 'href' in to && (to.href as string).startsWith('#')
const path = isHash ? config.app.baseURL + (to as any).href : joinURL(config.app.baseURL, to.fullPath)
reloadNuxtApp({ path, persistState: true })
}

nuxtApp.hook('app:manifest:update', () => {
router.beforeResolve(reloadAppAtPath)
})
nuxtApp.hook('app:manifest:update', () => {
router.beforeResolve(reloadAppAtPath)
})

router.onError((error, to) => {
if (chunkErrors.has(error)) {
reloadAppAtPath(to)
}
})
}
router.onError((error, to) => {
if (chunkErrors.has(error)) {
reloadAppAtPath(to)
}
})
},
})
Loading
Loading