,
params: params as Ref,
run: requestInstance.run,
runAsync: requestInstance.runAsync,
mutate: requestInstance.mutate,
cancel: requestInstance.cancel,
refresh: requestInstance.refresh,
refreshAsync: requestInstance.refreshAsync,
}
}
const usePolling: Plugin> = (request, { pollingInterval, pollingWhenHidden = true }) => {
if (!pollingInterval) return {}
let timer: number, docVisibleWatcher: () => void
const stop = () => {
if (timer) {
clearTimeout(timer)
}
docVisibleWatcher?.()
}
return {
before() {
stop()
},
finally() {
if (!pollingWhenHidden && !documentVisibility.value) {
docVisibleWatcher = whenDocumentVisible(request.refresh)
return
}
timer = setTimeout(request.refresh, pollingInterval)
},
cancel() {
stop()
},
}
}
const useDelay: Plugin> = (request, { loadingDelay = 0 }) => {
if (!loadingDelay) return {}
let timer: number
const clear = () => {
if (timer) {
clearTimeout(timer)
}
}
return {
before() {
clear()
timer = setTimeout(() => {
request.state.loading = true
}, loadingDelay)
return {
loading: false,
}
},
finally() {
clear()
},
cancel() {
clear()
},
}
}
const useAuto: Plugin> = (request, { ready = ref(true), immediate = false, refreshDeps = [] }) => {
if (refreshDeps) {
watch(refreshDeps, () => {
request.refresh()
})
}
if (immediate) {
if (ready.value) request.refresh()
else {
watch(ready, (nv) => {
if (nv) request.refresh()
})
}
}
return {
before() {
if (!ready || !ready.value) {
return {
stopNow: true,
}
}
},
}
}
================================================
FILE: packages/sharelist-manage/src/hooks/useScroll.ts
================================================
import { ref, Ref, reactive, UnwrapRef } from 'vue'
interface RequestOptions {
immediate?: boolean
isNoMore?: (data?: T) => boolean
onSuccess?: (data: T) => void
onError?: (e: Error) => void
target?: Ref
threshold?: number
}
interface actions {
setNode(el: Element): void
scrollTo(pos: number): void
cancel: () => void
checkScroll: () => void
isScroll: Ref
}
type useScrollOption = {
list: any[]
[key: string]: any
}
export const useScroll = >(
service: (args?: T) => Promise,
options: RequestOptions = {},
): actions => {
let el: Element
const isScroll = ref(false)
const setNode = (v: Element) => {
if (el) {
cancel()
}
el = v
el.addEventListener('scroll', onDomScroll)
}
const cancel = () => {
el?.removeEventListener('scroll', onDomScroll)
}
const onDomScroll = throttle(() => {
const { clientHeight, scrollTop, scrollHeight } = el
isScroll.value = scrollTop > 0
if (scrollHeight - scrollTop - clientHeight < (options?.threshold || 100)) {
service()
}
}, 200)
const scrollTo = (v: number) => {
if (el) {
el.scrollTo({ top: v })
}
}
const checkScroll = () => {
const { clientHeight, scrollTop, scrollHeight } = el
if (clientHeight == scrollHeight) service()
}
return {
setNode,
cancel,
scrollTo,
isScroll,
checkScroll,
}
}
const throttle = function (fn: () => any, delay = 0) {
let now: number,
last = 0,
timer: number | null,
context: any,
args: Array | null
const later = function () {
last = now
fn.apply(context, <[]>args)
timer = null
context = args = null
}
const listen = function (this: any, ...rest: Array) {
args = rest
now = Date.now()
//剩余时间
const remaining = delay - (now - last)
if (remaining <= 0 || remaining > delay) {
if (timer) {
clearTimeout(timer)
timer = null
}
last = now
fn.apply(this, <[]>rest)
if (!timer) context = args = null
} else if (!timer) {
timer = setTimeout(later, remaining)
}
}
return listen
}
================================================
FILE: packages/sharelist-manage/src/hooks/useSetting.ts
================================================
import { ref, Ref, reactive } from 'vue'
import { useApi, ReqResponse } from '@/hooks/useApi'
import { message } from 'ant-design-vue'
import { useBoolean } from './useHooks'
import { saveFile } from '../utils/format'
import useStore from '@/store/index'
type IUseSetting = {
(): IUseSettingResult
instance?: any
}
export interface IUseSettingResult {
configFields: Ref>
signout(): any
reload(): any
loginState: Ref
isLoading: Ref
getValue(key: string): any
config: any
setConfig(data: ISetting, msg?: string): Promise
getConfig(token: string): void
exportConfig(): void
clearCache(): void
getPlugin(id: string): Promise
setPlugin(id: string, data: string): Promise
removePlugin(id: string): Promise
upgradePlugin(id: string): Promise
reloadConfig(): void
}
export type ConfigFieldItem = {
code: string
label: string
help?: string
secret?: boolean
type: 'string' | 'number' | 'boolean' | 'option' | 'array' | 'textarea'
handler?: (...rest: any) => void
validator?: (...rest: any) => boolean
}
type fieldGroup = {
title: string
children: Array
}
const fields: Array = [
{
title: '常规',
children: [
{ code: 'title', label: '网站标题', type: 'string' },
{
code: 'manage_path',
label: '后台地址',
type: 'string',
help: '地址必须以 / 开头',
validator: (val) => /^\//.test(val),
handler: (nv: string, ov: string) => (location.href = location.href.replace(ov, nv)),
},
{ code: 'token', label: '后台密码', type: 'string', secret: true },
{ code: 'proxy_enable', label: '全局代理', type: 'boolean' },
{ code: 'index_enable', label: '目录浏览', type: 'boolean' },
{
code: 'proxy_override_content_type',
label: '代理时重写Content-Type',
help: '此项是为了兼容某些挂载源,因返回内容的content type异常,导致无法在线播放的问题。',
type: 'boolean',
},
{
code: 'anonymous_download_enable',
label: '允许下载',
type: 'boolean',
help: '禁用此项后,预览也将不可用。',
},
{
code: 'expand_single_disk',
label: '展开单一挂载盘',
help: '只有一个挂载盘时,直接展示改挂载盘内容。',
type: 'boolean',
},
{
code: 'per_page',
label: '列表分页大小',
help: '设置分页将自动禁用缓存。某些挂载源可能不支持自定义分页大小。0 表示不分页。',
type: 'number',
},
],
},
{
title: '外观',
children: [
{ code: 'theme', label: '主题', type: 'option' },
{ code: 'script', label: '自定义脚本', type: 'textarea' },
{ code: 'style', label: '自定义样式', type: 'textarea' },
],
},
{
title: '传输设置',
children: [
{ code: 'proxy_url', label: '代理地址', help: '当前支持 HTTP/HTTPS 代理。', type: 'string' },
{ code: 'plugin_source', label: '插件源', type: 'option' },
],
},
{
title: 'WebDAV',
children: [
{ code: 'webdav_enable', label: '启用 WebDAV', type: 'boolean' },
{ code: 'webdav_path', label: 'WebDAV 路径', type: 'string' },
{ code: 'webdav_proxy', label: 'WebDAV 代理', type: 'boolean' },
{ code: 'webdav_user', label: 'WebDAV 用户名', type: 'string' },
{ code: 'webdav_pass', label: 'WebDAV 密码', type: 'string' },
],
},
]
export const useSetting: IUseSetting = (): IUseSettingResult => {
if (useSetting.instance) {
return useSetting.instance
}
const request = useApi()
const store = useStore()
const config: ISetting = reactive({})
const [isLoading, { setFalse: hideLoading }] = useBoolean(true)
const loginState = ref(0)
const configFields: Ref> = ref(fields)
const getConfig = (val: string) => {
store.saveToken(val)
request.setting().then((resp: ReqResponse) => {
if (resp.error) {
if (resp.error.code == 401) {
store.saveToken('')
loginState.value = 2
if (resp.error.message) {
message.error(resp.error.message)
}
}
} else {
loginState.value = 1
updateSetting(resp.data as ISetting)
}
hideLoading()
})
}
const setConfig = (data: ISetting, msg = '已保存') => {
// console.log(data)
return request.saveSetting(data).then((resp: ReqResponse) => {
if (resp.error) {
message.error(resp.error.message || 'error')
} else {
updateSetting(resp.data as ISetting)
// return Promise.resolve(true)
message.success(msg)
}
})
}
const reloadConfig = () => {
getConfig(store.accessToken)
}
const updateSetting = (data: ISetting) => {
for (const i in data) {
config[i] = data[i]
}
configFields.value = [...fields, ...config.pluginConfig.map((i: any) => ({ title: i.name, children: i.config }))]
}
const getValue = (code: string) => {
return config[code]
}
const signout = () => {
store.removeToken()
loginState.value = 2
Object.keys(config).forEach((key) => Reflect.deleteProperty(config, key))
}
const reload = () => {
request.reload().then((resp: any) => {
// hidden()
if (resp.error) {
message.error(resp.error?.message)
} else {
message.success('操作成功')
}
})
}
const clearCache = () => {
// const hidden = message.loading('正在清除缓存', 0)
request.clearCache().then((resp: any) => {
// hidden()
if (resp.error) {
message.error(resp.error?.message)
} else {
message.success('操作成功')
}
})
}
const exportConfig = () => {
request.exportSetting().then((resp: any) => {
// hidden()
if (resp.error) {
message.error(resp.error?.message)
} else {
saveFile(JSON.stringify(resp.data), 'config.json')
}
})
}
const getPlugin = async (id: string): Promise => {
return request.plugin(id).then((resp: any) => {
if (resp.error) {
message.error(resp.error?.message)
} else {
return resp.data
}
})
}
const setPlugin = async (id: string, data: string): Promise => {
const res = await request.savePlugin({ id, data })
if (res.error) {
message.error(res.error?.message)
throw new Error(res.error?.message)
} else {
message.success('保存成功')
reloadConfig()
}
}
const removePlugin = async (id: string): Promise => {
const res = await request.removePlugin(id)
if (res.error) {
message.error(res.error?.message)
//throw new Error(res.error?.message)
} else {
message.success('删除成功')
reloadConfig()
}
}
const upgradePlugin = async (id: string): Promise => {
const res = await request.upgradePlugin(id)
if (res.error) {
message.error(res.error?.message)
//throw new Error(res.error?.message)
} else {
message.success('更新成功')
reloadConfig()
}
}
if (!config.token && store.accessToken) {
getConfig(store.accessToken)
} else {
loginState.value = 2
hideLoading()
}
return (useSetting.instance = {
signout,
reload,
loginState,
isLoading,
getValue,
configFields,
config,
setConfig,
getConfig,
exportConfig,
clearCache,
getPlugin,
setPlugin,
removePlugin,
upgradePlugin,
reloadConfig,
})
}
export const useConfig: IUseSetting = (): any => {
const config: Record = reactive({})
const request = useApi()
request.config().then((resp: ReqResponse) => {
if (!resp.error) {
for (const i in resp.data) {
config[i] = resp.data[i]
}
}
})
return { config }
}
================================================
FILE: packages/sharelist-manage/src/hooks/useStore.ts
================================================
import { provide, inject, Ref, ref } from 'vue'
type InjectType = 'root' | 'optional'
export interface FunctionalStore {
(): T
key?: symbol
root?: T
}
export const regStore = (store: FunctionalStore): T => {
if (!store.key) {
store.key = Symbol('functional store')
}
const depends = store()
provide(store.key, depends)
return depends
}
export const useStore = (store: FunctionalStore, type?: InjectType): any => {
const key = store.key
const root = store.root
switch (type) {
case 'optional':
return inject(key) || store.root || null
case 'root':
if (!store.root) store.root = store()
return store.root
default:
if (inject(key)) {
return inject(key)
}
if (root) return store.root
throw new Error(`状态钩子函数${store.name}未在上层组件通过调用useProvider提供`)
}
}
export const useAsync = (cb: (() => Promise) | Promise): Ref => {
const val = ref()
Promise.resolve(typeof cb === 'function' ? cb() : cb).then((resp) => {
val.value = resp
})
return val
}
const jsbridge = {
call: (name: string, param?: any, callback?: (value?: unknown) => void) => name,
getGrayScaleValue: (v: any) => v,
}
type BridgeValue = {
returnValue: string
}
type BridgeFunctions = 'getGrayScaleValue'
export const useBridgeValue = (
fn: BridgeFunctions,
params: P,
): { value: Ref; ready: Ref } => {
const value: Ref = ref()
const ready = ref(false)
Promise.resolve(jsbridge[fn](params)).then((resp: T) => {
value.value = resp
ready.value = true
})
return { value, ready }
}
type GrayParams = 'code1' | 'code2'
type GrapValue = {
returnValue: string
}
const { ready, value } = useBridgeValue('getGrayScaleValue', 'code')
type ICdpSpaceInfo = {
returnValue: string
}
function getCdpSpaceInfo(spaceCode: string): Promise {
return new Promise((resolve) => {
jsbridge.call('getCdpSpaceInfo', { spaceCode }, (resp: unknown) => {
resolve(resp as ICdpSpaceInfo)
})
})
}
================================================
FILE: packages/sharelist-manage/src/hooks/useUrlState.ts
================================================
import { useRouter, useRoute } from 'vue-router'
import { reactive, watch } from 'vue'
import { useState } from './useHooks'
type initial = {
params: Record
query: Record
}
export default ({ params: initialParams, query: initialQuery }: initial = { params: {}, query: {} }): any => {
const router = useRouter()
const route = useRoute()
const [params, updateParams] = useState({
...initialParams,
...route.params,
})
const [query, updateQuery] = useState({
...initialQuery,
...route.query,
})
const setQuery = (data: Record) => {
router.push({
query: {
...route.query,
...updateQuery(data),
},
})
}
const setParams = (data: Record) => {
router.push({
...route.params,
...updateParams(data),
})
}
const setPath = (path: string) => {
router.push(path)
}
watch(() => route.params, updateParams)
watch(() => route.query, updateQuery)
watch(query, (nv) => {
console.log('>>>', nv)
})
return { params, query, setQuery, setParams, setPath }
}
================================================
FILE: packages/sharelist-manage/src/hooks/useWorker.ts
================================================
const fnCache = new WeakMap()
type Fn = (...args: any[]) => any
const WORKER_SCRIPT = () => {
const methodsMap: Record = {}
function invoke(name: string, params: unknown[], id: string) {
try {
if (!methodsMap[name]) {
throw new Error('function ' + name + ' is not registered.')
}
const result = methodsMap[name].apply(null, params)
Promise.resolve(result)
.then(function onresolve(res) {
self.postMessage(
JSON.stringify({
data: res,
name: name,
id: id,
}),
)
})
.catch(function onerror(error) {
throw error
})
} catch (error) {
throw error
}
}
self.onmessage = function (e) {
const data = JSON.parse(e.data)
const type = data.type
const name = data.name
switch (type) {
case 'add':
methodsMap[name] = eval(data.code)
break
case 'remove':
if (methodsMap[name]) {
delete methodsMap[name]
}
break
case 'clear':
methodsMap = {}
break
case 'invoke':
var params = data.params
var id = data.id
invoke(name, params, id)
break
}
}
}
class WorkerFactory {
worker: Worker
constructor() {
const url = URL.createObjectURL(new Blob([`(${WORKER_SCRIPT.toString()})()`]))
this.worker = new Worker(url)
}
}
export const useWorker = (fn: Fn) => {
if (useWorker.instance) {
return useWorker.instance
}
const name = fn.name
const url = URL.createObjectURL(new Blob([WORKER_SCRIPT.toString()]))
if (!fnCache.has(fn)) {
const url = URL.createObjectURL(new Blob([__WORKER_SCRIPT__]))
fnCache.set(fn, {
name,
})
}
}
================================================
FILE: packages/sharelist-manage/src/hooks/utils.ts
================================================
import { getCurrentInstance, isRef, onMounted as vueOnMounted, onUnmounted as vueOnUnmounted, Ref } from 'vue'
export const onMounted = (cb: () => any): void => {
const instance = getCurrentInstance()
if (instance) {
if (instance?.isMounted) {
cb()
} else {
vueOnMounted(cb)
}
}
}
export function onUnmounted(cb: () => any): void {
if (getCurrentInstance()) {
vueOnUnmounted(cb)
}
}
================================================
FILE: packages/sharelist-manage/src/index.html
================================================
sharelist
================================================
FILE: packages/sharelist-manage/src/main.ts
================================================
import { createApp, h } from 'vue'
import App from './App'
import router from './router'
import { message, Spin, ConfigProvider } from 'ant-design-vue'
import { createPinia } from 'pinia'
import piniaPersist from 'pinia-plugin-persist'
import apis from '@/config/api'
import { createApi } from '@/hooks/useApi'
import useStore from '@/store/index'
// import 'ant-design-vue/dist/antd.variable.less'
import '@/assets/style/index.less'
import { LoadingOutlined } from '@ant-design/icons-vue'
Spin.setDefaultIndicator({
indicator: h(LoadingOutlined, {
style: {
fontSize: '24px',
},
spin: true,
}),
})
ConfigProvider.config({
theme: {
primaryColor: 'rgb(100,58,218)',
},
autoInsertSpaceInButton: false,
})
const pinia = createPinia()
pinia.use(piniaPersist)
createApi(apis, {
onReq(params, options) {
if (options.token) {
params.headers['Authorization'] = useStore().accessToken
}
},
})
createApp(App)
.use(router)
.use(pinia)
// .use(
// createApi(apis, {
// onReq(params, options) {
// if (options.token) {
// params.headers['Authorization'] = useStore().accessToken
// }
// },
// }),
// )
.provide('$message', message)
.mount('#app')
================================================
FILE: packages/sharelist-manage/src/router/index.ts
================================================
import { createRouter, createWebHashHistory, createWebHistory, RouteRecordRaw, onBeforeRouteLeave } from 'vue-router'
const routes: Array = [
{
path: '/',
component: () => import('../views/home'),
redirect: '/drive/folder',
children: [
{
path: '/general',
name: 'general',
component: () => import('../views/general'),
},
{
path: '/drive/folder:path(.*)',
name: 'drive',
component: () => import('../views/disk'),
/*
children: [
{
path: 'folder:path(.*)',
name: 'file',
component: () => import('../views/disk/files'),
},
],
*/
},
{
path: '/plugin',
name: 'plugin',
component: () => import('../views/plugin'),
},
],
},
]
const router = createRouter({
history: createWebHistory((window as any).MANAGE_BASE),
routes,
})
export default router
================================================
FILE: packages/sharelist-manage/src/store/index.ts
================================================
import { defineStore } from 'pinia'
export default defineStore('sharelist_manage', {
state: () => ({
accessToken: '',
layout: 'list',
theme: 'light',
path: '',
}),
actions: {
saveToken(token: string) {
this.accessToken = token
},
removeToken() {
this.accessToken = ''
},
setLayout(val: string) {
this.layout = val
},
savePath(input: string) {
this.path = input
},
},
persist: {
enabled: true,
strategies: [
{ storage: localStorage, paths: ['layout'] },
{ storage: sessionStorage, paths: ['accessToken'] },
],
},
})
================================================
FILE: packages/sharelist-manage/src/types/IDrive.ts
================================================
type DrivePath = {
protocol: string
[key: string]: string | number
}
declare type DriverField = {
key: string
label: string
value?: string | number | boolean
options?: Array
type?: 'string' | 'hidden' | 'number' | 'boolean' | 'list'
help?: string
fields?: Array
required?: boolean
}
declare type IDrive = {
name: string
[key: string]: string | number
}
declare type IPlugin = {
name: string
id: string
[key: string]: string | number
}
declare type DriverGuide = {
key?: string
label?: string
fields: Array
}
declare type Driver = {
protocol: string
name?: string
guide?: Array
}
declare type IFile = {
id: string
name: string
size: number
type: 'folder' | 'file' | 'drive'
ctime: number
mtime: number
path: string
extra?: Record
[key: string]: any
}
================================================
FILE: packages/sharelist-manage/src/types/shim.d.ts
================================================
declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
declare type ISetting = {
title?: string
index_enable?: boolean
default_ignores?: Array
[key: string]: any
}
================================================
FILE: packages/sharelist-manage/src/types/source.d.ts
================================================
declare module '*.json'
declare module '*.png'
declare module '*.jpg'
declare module 'vue-infinite-scroll'
================================================
FILE: packages/sharelist-manage/src/utils/format.ts
================================================
const EXT_IMAGE = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'wmf', 'tif', 'svg', 'webp']
const EXT_AUDIO_SUPPORT = ['mp3', 'm4a', 'acc', 'wav', 'ogg', 'flac']
const EXT_VIDEO_SUPPORT = ['mp4', 'mpeg', '3gp', 'mkv']
export const getFileType = (v: string, type = 'file'): string => {
if (type == 'folder' || type == 'drive') {
return type
} else {
if (v) v = v.toLowerCase().split('.').pop() || ''
if (['mp4', 'mpeg', 'wmv', 'webm', 'avi', 'rmvb', 'mov', 'mkv', 'f4v', 'flv'].includes(v)) {
return 'video'
} else if (['mp3', 'm4a', 'wav', 'wma', 'ape', 'flac', 'ogg'].includes(v)) {
return 'audio'
} else if (['doc', 'docx', 'wps'].includes(v)) {
return 'word'
} else if (['ppt', 'pptx'].includes(v)) {
return 'ppt'
} else if (['pdf'].includes(v)) {
return 'pdf'
} else if (['xls', 'xlsx', 'pdf', 'txt', 'yaml', 'yml', 'ini', 'cfg', 'xml', 'md'].includes(v)) {
return 'doc'
} else if (EXT_IMAGE.includes(v)) {
return 'image'
} else if (
[
'js',
'ts',
'css',
'html',
'c',
'h',
'cpp',
'py',
'java',
'jsp',
'php',
'cs',
'go',
'swift',
'vue',
'rs',
'asp',
'sql',
].includes(v)
) {
return 'code'
}
// else if (['zip', 'rar', '7z', 'tar', 'gz', 'gz2'].includes(v)) {
// return 'archive'
// }
else {
return 'file'
}
}
}
export const byte = (v: number, fixed?: number): string => {
if (v === undefined || v === null || isNaN(v)) {
return '-'
}
let lo = 0
while (v >= 1024) {
v /= 1024
lo++
}
const val = Math.floor(v * 100) / 100
return (fixed ? val.toFixed(fixed) : val) + ' ' + ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'][lo]
}
const fix0 = (v: any) => (v > 9 ? v : '0' + v)
export const time = (v: number): string => {
if (!v) return '-'
const date = new Date(v)
const thisYear = new Date().getFullYear()
let ret: string =
fix0(date.getMonth() + 1) + '/' + fix0(date.getDay()) + ' ' + fix0(date.getHours()) + ':' + fix0(date.getMinutes())
if (thisYear != date.getFullYear()) {
ret = date.getFullYear() + '/' + ret
}
return ret
}
export const formatFile = (file: IFile | Array): IFile | Array => {
if (Array.isArray(file)) {
file.forEach((i) => {
i.ext = i.name.split('.').pop()
i.mediaType = getFileType(i.name, i.type)
if (i.ctime) i.ctimeDisplay = time(i.ctime)
if (i.size) i.sizeDisplay = byte(i.size)
})
} else {
file.ext = file.name.split('.').pop()
file.mediaType = getFileType(file.name, file.type)
if (file.ctime) file.ctimeDisplay = time(file.ctime)
if (file.size) file.sizeDisplay = byte(file.size)
}
return file
}
export const isMediaSupport = (name: string, type: 'audio' | 'video' | 'image'): boolean => {
const ext: string = name.split('.').pop() || ''
if (type == 'audio' && EXT_AUDIO_SUPPORT.includes(ext)) {
return true
} else if (type == 'video' && EXT_VIDEO_SUPPORT.includes(ext)) {
return true
} else if (type == 'image' && EXT_IMAGE.includes(ext)) {
return true
}
return false
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const isType = (type: string) => (obj: any) => Object.prototype.toString.call(obj) === `[object ${type}]`
export const isArray = isType('Array')
export const isObject = isType('Object')
export const isBlob = isType('Blob')
export const isString = (v: string): boolean => typeof v == 'string'
export const getBlob = (data: string, filename: string): Blob | undefined => {
let blob
try {
blob = new Blob([data], { type: 'application/octet-stream' })
} catch (e) {
/**/
}
return blob
}
export const saveFile = (data: Blob | string, filename: string): void => {
let blob: Blob | undefined
if (isString(data as string)) {
blob = getBlob(data as string, filename)
} else {
blob = data as Blob
}
if (blob && isBlob(blob)) {
const URL = window.URL || window.webkitURL
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = filename
const evt = document.createEvent('MouseEvents')
evt.initEvent('click', false, false)
// link.click()
link.dispatchEvent(evt)
URL.revokeObjectURL(link.href)
}
}
================================================
FILE: packages/sharelist-manage/src/views/disk/index.less
================================================
.drive{
display: flex;
flex-direction: column;
height:100vh;
&.drive--lite{
height:auto;
}
.drive__header-assistant{
display: flex;
justify-content: space-between;
margin:0 24px 8px 32px;
padding-bottom:12px;
border-bottom: 1px solid var(--primary-hover-bg-color);
font-size:13px;
}
.ant-checkbox-inner{
border-radius: 50%;
box-sizing: border-box;
}
.ant-checkbox-indeterminate{
.ant-checkbox-inner{
background-color: var(--ant-primary-color);
border-color: var(--ant-primary-color);
&:after{
// background-color: var(--context-background);
background-color: #fff;
height:2px;
}
}
}
.drive__header{
display: flex;
align-items: center;
justify-content: space-between;
color: rgba(0, 0, 0, .65);
font-size: 22px;
padding:32px 32px;
z-index:1;
// position: sticky;
// top:0;
.drive__header-back {
margin-right: 1em;
}
// border-bottom:1px solid #f5f6f7;
}
// .drive-breadcrumb{
// overflow-x:auto ;
// }
.drive__actions{
flex:none;
color: var(--context-1);
.ant-badge-dot{
// background:var(--primary-background);
}
}
.drive-search{
max-width:560px;
width:90%;
margin: auto;
transform: translate(0,20vh);
}
.drive-body-wrap{
flex: 1 1 auto;
overflow-y: auto;
position: relative;
}
.drive-body{
flex:1 1 auto;
.item{
display: flex;
width:100%;
justify-content: space-between;
align-items: center;
padding:12px 0;
transition: all 0.3s;
margin-bottom:1px;
cursor: pointer;
position: relative;
color:var(--primary-text-color);
&:hover{
background-color: var(--context-hover);
.item-check{
opacity: 1;
}
}
.item-check{
padding:0 8px;
opacity: 0;
transition:opacity 0.3s;
}
&.item--checked{
background-color: var(--primary-hover-theme-color);
.item-check{
opacity: 1;
}
}
&.item--disabled{
color: var(--context-3);
cursor:not-allowed;
pointer-events: none;
}
.item-icon__ext--md{
display: none;
}
.item-icon__ext--sm{
display: none;
}
.item-info{
padding:0 6px;
color:rgba(0,0,0,.5);
flex:0 0 auto;
position:absolute;
right:0;
top:0;
}
.item-name{
flex: 1;
min-width: 0;
width:100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
// display: flex;
position: relative;
}
.item-icon{
margin-right: 16px;
flex:none;
width:36px;
position: relative;
}
.item-thumb{
width:100%;
height:30px;
background-repeat: no-repeat;
background-size:cover;
background-position: center center;
flex:none;
border-radius: 5px;
}
.item-icon__ext{
position: absolute;
font-size: 12px;
bottom: 5px;
color:inherit;
text-transform: uppercase;
transform: scale(0.8);
transform-origin:center;
// width:42px;
text-align: center;
width:100%;
font-weight: bold;
// transform-origin: left;
// left: 16px;
}
.item-meta{
flex:auto;
display: flex;
justify-content:space-between;
align-items: center;
overflow: hidden;
color:inherit;
}
.item-ctime{
flex: 0 0 auto;
// padding:0 24px;
// width: 200px;
color:var(--context-2);
font-size: 12px;
text-align: left;
}
.item-size{
// color:rgba(37, 38, 43, 0.72);
color:var(--context-2);
font-size: 12px;
text-align: right;
flex:0 0 80px;
}
}
}
.drive-body-mask{
position: absolute;
top:0;left:0;
width:100%;
height:100%;
opacity: 0;
}
.drive-body--padding{
padding:0 24px;
}
.drive-body.drive-body--grid{
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
grid-gap: 8px;
.item{
padding-left:8px;
padding-right:8px;
flex-direction: column;
.item-check{
position: absolute;
right:0px;
top:0px;
}
.item-icon{
width:auto;
margin-right:0;
margin-bottom:12px;
}
.item-meta{
flex-direction: column;
// align-items: flex-start;
align-items: center;
width:100%;
overflow: hidden;
}
.item-thumb{
height:64px;
width:96px;
}
.item-name{
text-align: center;
font-size:13px;
}
.item-ctime{
// display: none;
color:var(--context-3);
}
.item-size{
display: none;
}
.item-icon__ext{
font-size:15px;
}
}
}
}
.drive-toolbar-wrap{
position: absolute;
left: 50%;
bottom: 0;
transform: translateX(-50%);
z-index: 1;
transition: all .3s ease;
opacity: 0;
pointer-events: none;
&.show{
opacity: 1;
pointer-events: auto;
bottom:32px;
}
}
.drive-toolbar{
display: flex;
align-items: center;
padding: 8px 16px;
border-radius: 10px;
background: var(--dark-background);
overflow: visible;
user-select: none;
box-shadow: 0 0 1px 1px rgb(28 28 32 / 5%), 0 8px 24px rgb(28 28 32 / 12%);
color:#fff;
border: 1px solid var(--divider-1);
.drive-toolbar-item{
font-size:16px;
padding:8px;
color:#fff;
margin:0 6px;
border-radius: 6px;
&:hover{
background-color: var(--color-white-4);
}
}
}
.drive-search{
.tips{
font-size: 12px;
color:var(--context-2);
margin-top: 1em;
}
.search-history__header{
font-size: 12px;
color:var(--context);
margin:1em 0;
}
.drive-search-input{
padding:8px;
font-size:14px;
}
input{
border:none;
background-color: transparent;
flex:auto;
// border-bottom: 1px solid var(--context-4);
outline:none;
}
.search-history__body{
}
.search-history__item{
display: flex;
align-items: center;
width:100%;
justify-content: space-between;
padding:0.8em 8px;
border-radius: 3px;
cursor: pointer;
&:hover{
background-color:var(--context-hover);
}
color:var(--context_primary);
}
}
// .badge
================================================
FILE: packages/sharelist-manage/src/views/disk/index.tsx
================================================
import { h, ref, Ref, defineComponent, watch, onMounted, onUnmounted, nextTick, toRef, computed, withModifiers } from 'vue'
import useStore from '@/store/index'
import { storeToRefs } from 'pinia'
import { Spin, Modal, Dropdown, Popover, Menu, Badge, Checkbox, Tooltip, InputSearch, RadioGroup, Input } from 'ant-design-vue'
import Icon from '@/components/icon'
import useDisk, { IFile } from './partial/useDisk'
import './index.less'
import { isMediaSupport } from '@/utils/format'
import MediaPlayer, { usePlayer } from '@/components/player'
import Breadcrumb from './partial/breadcrumb'
import Error from './partial/error'
import { useSetting } from '@/hooks/useSetting'
import { InfoCircleOutlined, LoadingOutlined, ScissorOutlined, EditOutlined, HddOutlined, FolderAddOutlined, EllipsisOutlined, DeleteOutlined, CloudSyncOutlined, PlusOutlined, CloudDownloadOutlined, DownloadOutlined, CloseCircleFilled, SwapOutlined, SortDescendingOutlined, CalendarOutlined, FieldBinaryOutlined, ArrowUpOutlined, ArrowDownOutlined, ClockCircleOutlined, MinusCircleOutlined } from '@ant-design/icons-vue'
import useConfirm, { useApiConfirm } from '@/hooks/useConfirm'
import { useRoute, onBeforeRouteLeave } from 'vue-router'
import { useApi } from '@/hooks/useApi'
import { useActions } from './partial/action'
import Task from './partial/task'
import { useBoolean } from '@/hooks/useHooks'
import { useLocalStorageState } from '@/hooks/useLocalStorage'
import { Upload } from './partial/upload'
import { useScroll } from '@/hooks/useScroll'
import { useClipboard } from '@/hooks/useClipboard'
import { showImage } from '@/components/image'
export default defineComponent({
setup() {
const { setConfig, clearCache, config } = useSetting()
const searchHistory = useLocalStorageState>('search_history', [])
const confirmClearCache = useConfirm(clearCache, '确认', '确认清除缓存?')
const route = useRoute()
const store = useStore()
const diskIntance = useDisk()
const { loading, files, error, setPath, paths, loadMore, diskConfig, current: currentDisk, setAuth, setSort, sortConfig, onUpdate } = diskIntance
const { rename, move, remove, mkdir, flashDownload, uploadConfirm, addDisk, setDisk, remoteDownload, showInfo } = useActions(diskIntance)
const { setPlayer } = usePlayer('player')
const driveEl: Ref = ref()
const { node: pasteEl } = useClipboard((files) => {
let { id } = diskConfig.value
let dest = '/' + [...paths.value].join('/')
uploadConfirm(files, dest, id)
}, 'file')
const { scrollTo, setNode, cancel: cancelScroll, isScroll, checkScroll } = useScroll(loadMore)
onMounted(() => {
setNode(driveEl.value)
onUpdate(() => {
setTimeout(checkScroll, 0)
})
})
onUnmounted(() => {
cancelScroll?.()
})
const onClick = (data: IFile) => {
if (data.type == 'folder' || data.type == 'drive') {
let target: any = {
id: data.id
}
if (!data.path && !currentDisk.search) {
target.path = currentDisk.path + '/' + data.name
}
setPath(target)
} else if (data.type == 'file') {
let mediaType = data.mediaType
if ((data.mediaType == 'audio' || data.mediaType == 'video') && isMediaSupport(data.name, mediaType)) {
const list: Array = files.value.filter((i: IFile) => isMediaSupport(i.name, mediaType))
setPlayer({
list,
type: mediaType,
index: list.findIndex((i: IFile) => i.id == data.id),
})
}
else if (data.mediaType == 'image') {
const list: Array =
files.value.filter((i: IFile) => isMediaSupport(i.name, 'image'))
showImage(list.map(i => i.download_url), list.findIndex(i => i.id == data.id))
}
else {
window.open(data.download_url)
}
}
}
let currentFocusFile: Ref = ref()
const onAction = ({ key }: { key: any }) => {
targetBind.value = false
if (key == 'info') {
showInfo(currentFocusFile.value as IFile)
} else if (key == 'mount_drive') {
addDisk()
} else if (key == 'mkdir') {
mkdir(diskConfig.value)
} else if (key == 'config') {
let name = currentFocusFile.value?.name
let idx = config.drives.findIndex((i: IDrive) => i.name == name)
if (idx >= 0) {
setDisk(config.drives[idx], idx)
}
} else if (key == 'rename') {
rename(currentFocusFile.value as IFile)
} else if (key == 'move') {
move(currentFocusFile.value as IFile)
} else if (key == 'delete') {
remove(currentFocusFile.value as IFile)
} else if (key == 'upload') {
} else if (key == 'flash_upload') {
flashDownload(diskConfig.value)
} else if (key == 'remote_download') {
remoteDownload(diskConfig.value)
}
}
let isActionHide = true
let targetBind = ref(false)
const onContextChange = (visible: boolean) => {
console.log('action:', visible)
// isActionHide = true
if (visible) {
if (targetBind.value == false) {
currentFocusFile.value = undefined
}
//isActionHide = false
// onActionVisibleChange(visible)
// currentFocusFile.value = undefined
//
} else {
// currentFocusFile.value = undefined
// targetBind = false
targetBind.value = false
}
}
const onHover = (i: IFile | null, e?: MouseEvent) => {
//if (!actionVisible.value) {
//willShow = !!i
//if (willShow) {
console.log('on hover')
//已存在
if (currentFocusFile.value) {
}
currentFocusFile.value = i
if (i) {
targetBind.value = true
}
//}
// onActionVisibleChange()
//}
/*
if (i === null) {
currentFocusFile.value = undefined
} else {
if (!actionVisible.value) {
currentFocusFile.value = i
}
}*/
}
const download = (file: IFile | Array) => {
if (!Array.isArray(file)) {
file = [file]
}
file.forEach((i: IFile) => {
let a = document.createElement('a')
let e = document.createEvent('MouseEvents')
e.initEvent('click', false, false)
a.href = i.download_url // 设置下载地址
a.download = i.name
a.dispatchEvent(e)
})
}
const mainSlots = {
overlay: () => {
let isDriveLevel = diskConfig.value.isRoot
// setTimeout(() => {
// currentFocusFile.value = undefined
// }, 0)
// blank area
if (currentFocusFile.value || targetBind.value) {
if (isDriveLevel) {
return
} else {
return
}
} else {
if (isDriveLevel) {
return
} else {
return
}
}
}
}
watch(
route,
(nv) => {
if (route.name == 'drive') {
let target: any = {
path: '/' + (route.params.path as string).replace(/^\//, '')
}
if (route.query.search) {
target.search = route.query.search
}
setPath(target)
scrollTo(0)
// getFiles({ path: route.params.path as string })
}
},
{ immediate: true },
)
const onTagClick = ({ path, index }: any = {}) => {
if (path == '/') {
setPath({ path: '/' })
} else if (index < paths.value.length) {
setPath({ path: '/' + paths.value.slice(0, index).join('/') })
}
}
const onSelect = (i: any) => {
i.checked = !i.checked
}
const onUnselectAll = () => {
files.value.forEach((i: any) => (i.checked = false))
}
const onSelectAll = (e: { target: { checked: boolean } }) => {
let val = e.target.checked
files.value.forEach((i: any) => (i.checked = val))
}
const selectState = computed(() => {
let count = files.value.filter((i: IFile) => !!i.checked).length
let containDir = files.value.some((i: IFile) => !!i.checked && i.type == 'folder')
let containDrive = files.value.some((i: IFile) => !!i.checked && i.type == 'drive')
let containFile = files.value.some((i: IFile) => !!i.checked && i.type == 'file')
// 0 unselect, 1 partially selected, 2 select all,
let state = count == 0 ? 0 : count == files.value.length ? 2 : 1
return {
state, containDir, containDrive, count, total: files.value.length, containFile
}
})
const onToggleSearch = () => {
const onSearch = (value: string) => {
console.log(value)
if (value) {
// router.push({ path: router.currentRoute.value.path, query: { search: value } })
let historyRecords = [...searchHistory.value]
let idx = historyRecords.indexOf(value)
if (idx >= 0) {
historyRecords.splice(idx, 1)
}
historyRecords.unshift(value)
searchHistory.value = historyRecords
setPath({ search: value, path: currentDisk.path, id: currentDisk.id })
modal.destroy()
}
}
const remove = (value: string) => {
let idx = searchHistory.value.indexOf(value)
if (idx >= 0) {
searchHistory.value.splice(idx, 1)
}
}
const options: Array = []
const searchMode = diskConfig.value.search
const tips = ref(searchMode == 1 ? '* 仅支持全局搜索' : searchMode == 2 ? '* 仅支持搜索当前目录(不含子目录)' : '')
/*
if (searchMode == 1) {
options.push({ label: '所有文件', value: 'global' })
}
if (searchMode > 1) {
options.push({ label: '当前目录', value: 'local' })
}
const searchType = ref(options[0]?.value)
*/
const modal = Modal.confirm({
class: 'fix-modal--alone',
width: '560px',
maskClosable: true,
content: () => (
{/* */}
onSearch(e.target.value)} />
{tips.value ?
{tips.value}
: null}
{
searchHistory.value.map((i) => - onSearch(i)}>
{i}
remove(i), ['prevent', 'stop'])} /> )
}
{/* {options.length ?
searchType.value = e.target.value} name="radioGroup" > : null} */}
),
})
}
const changeView = () => {
let val = store.layout == 'list' ? 'grid' : 'list'
store.setLayout(val)
}
return () => (
{
error.code === 0 ? : null
}
)
},
})
================================================
FILE: packages/sharelist-manage/src/views/disk/partial/action/index.less
================================================
.file-item{
padding-right:8px;
overflow-x: hidden;
.ant-checkbox-inner{
border-radius: 50%;
box-sizing: border-box;
}
.file-item__rm{
opacity: 0;
transition: opacity 0.3s;
padding:8px;
cursor: pointer;
&:hover{
color:var(--color-red);
}
}
&:hover{
background-color: var(--primary-hover-bg-color);
.file-item__rm{
opacity: 1;
}
}
}
.setting{
.setting__item{
// display: flex;
// align-items: center;
padding:8px 0;
margin-top:8px;
.setting__item-label{
flex:none;
// width:100px;
font-size:12px;
color:var(--context-2);
display: block;
margin-bottom:0.6em;
}
}
&.small{
font-size:12px;
}
}
.file-detail__item{
padding:8px 16px;
.file-detail__item-label{
font-size:12px;
color:var(--context-3);
}
.file-detail__item-content{
font-size:14px;
color:var(--primary-text-color);
}
}
================================================
FILE: packages/sharelist-manage/src/views/disk/partial/action/index.tsx
================================================
import { defineComponent, withDirectives, ref, getCurrentInstance, Ref, computed, reactive } from "vue";
import { Input, Modal, message, Alert, Checkbox, InputNumber, Radio, Tooltip, Textarea } from 'ant-design-vue'
import useDisk from '../useDisk'
import { EditOutlined, ScissorOutlined, CloudDownloadOutlined, CloudUploadOutlined, DownloadOutlined, QuestionCircleOutlined } from '@ant-design/icons-vue'
import { useApi } from "@/hooks/useApi";
import { Meta, Tree } from '../meta'
import { useApiConfirm } from '@/hooks/useConfirm'
import { byte, getFileType, time } from '@/utils/format'
import { useUpload } from '../upload'
import Modifier from '../modifier'
import { useSetting } from '@/hooks/useSetting'
import './index.less'
import { useFocus } from '@/hooks/useDirective'
// import { IUseSettingResult } from '../useDisk'
interface IFileError extends IFile {
errorMessage?: string
}
const FileErrorList = defineComponent({
props: {
files: Array
},
emits: ['change'],
setup(props, { emit }) {
let unchecked: Ref | any> = ref(props.files?.map(i => true))
const toggle = (idx: number) => {
unchecked.value[idx] = !unchecked.value[idx]
emit('change', unchecked.value.reduce((t: Array, c: boolean, idx: number) => c ? t.concat(idx) : t, []))
}
return () =>
{
props.files?.map((i: any, idx) =>
toggle(idx)} class="flex flex--between file-item">
)
}
}
})
const FileSelect = defineComponent({
props: {
files: Array
},
emits: ['change'],
setup(props, { emit }) {
let unchecked: Ref | any> = ref(props.files?.map(i => true))
const toggle = (idx: number) => {
unchecked.value[idx] = !unchecked.value[idx]
emit('change', unchecked.value.reduce((t: Array, c: boolean, idx: number) => c ? t.concat(idx) : t, []))
}
return () =>
{
props.files?.map((i: any, idx) =>
toggle(idx)} class="flex flex--between file-item">
)
}
}
})
export const useActions = (diskIntance: any) => {
let { mutate, setAuth, current, setPath, reload } = diskIntance
const appContext = getCurrentInstance()?.appContext;
let request = useApi()
const { setConfig, clearCache, config } = useSetting()
const rename = (i: IFile) => {
let name = i.name
const onChange = (e: any) => {
name = e.target.value
}
const onSave = async () => {
if (name != i.name) {
let res = await request.fileUpdate({ id: i.id, name })
// console.log(res)
if (res.error) {
message.error(res.error.message)
throw new Error()
} else {
i.name = name
mutate(i)
modal.destroy()
}
}
}
const modal = Modal.confirm({
class: 'pure-modal',
width: '500px',
closable: true,
appContext,
autoFocusButton: null,
title: 重命名
,
content: () => (
{useFocus(, true)}
),
onOk: onSave,
})
}
const move = (file: IFile | Array) => {
let dest: String
let files: Array = Array.isArray(file) ? file : [file]
let srcPath = diskIntance.current.path
let isTransfer = ref(false)
let threadNum = ref(1)
let buttonProps = reactive({ disabled: true })
let count = files.length
let isSingleTask = count == 1
let error: Array = []
let current = ref(0)
let cancelFlag = ref(false)
let key = `${Math.random()}`
let targetMultiThreading = ref(false)
// 排除 当前目录 及 子目录
let excludePath = [
srcPath,
...files.map(i => i.id)
]
const onSelect = (e: IFile, targetDiskConfig: any) => {
let autoExpand = config.expand_single_disk
let diskCount = config.drives.length
let destPath = e.path
let destIsRoot = destPath == '/'
// 根目录是磁盘列表
let rootIsDiskList = (!autoExpand || diskCount > 1)
buttonProps.disabled = destPath == srcPath || (destIsRoot && rootIsDiskList)
isTransfer.value = rootIsDiskList && srcPath.split('/')[1] != destPath.split('/')[1] && !destIsRoot
dest = e.id
targetMultiThreading.value = targetDiskConfig.multiThreading
}
const execMove = async () => {
message.loading({ content: `正在${isTransfer.value ? '创建迁移任务' : '移动'}`, key, duration: 0 });
for (let i of files as Array) {
if (cancelFlag.value) return
message.loading({ content: `[${current.value + 1}/${count}]` + '正在移动:' + i.name, key, duration: 0 });
let res = await request.fileUpdate({ id: i.id, dest, threadNum: threadNum.value })
if (res.error) {
i.error = res.error.message
error.push(i.id)
} else {
if (!isTransfer.value) mutate(i, true)
}
current.value++
}
let errorFiles = files.filter((i: IFile) => error.includes(i.id))
if (errorFiles.length) {
if (isSingleTask) {
message.error({ content: (files as Array)[0].error, key, duration: 1 });
} else {
message.success({ content: '操作完成', key, duration: 0.01 });
errorModal(errorFiles)
}
} else {
message.success({ content: isTransfer.value ? '迁移任务创建成功' : '操作完成', key, duration: 1 });
}
}
const errorModal = (errorFiles: Array) => {
let retryList: Array = []
Modal.confirm({
class: 'pure-modal',
width: '500px',
closable: true,
title: () => 移动
,
okText: '重试',
content: () => (
),
onOk: () => {
if (retryList.length) {
files = (files as Array).filter((_, idx: number) => retryList.includes(idx))
//move((files as Array).filter((_, idx: number) => retryList.includes(idx)))
execMove()
}
},
})
}
const modal = Modal.confirm({
class: 'pure-modal',
width: '500px',
closable: true,
title: () => 移动
,
content: () => (
{files.length == 1 ?
)[0]} /> : null}
{
isTransfer.value ? [
,
{
targetMultiThreading.value ?
线程数
{ threadNum.value = e as number }}>
: null
}
{/*
*/}
] : null
}
),
okButtonProps: buttonProps,
onOk: () => {
execMove()
},
})
}
const mkdir = (i: IFile) => {
let name = '新建文件夹'
const onChange = (e: any) => {
name = e.target.value
}
const onSave = async () => {
if (name) {
let res = await request.mkdir({ id: i.id, name })
if (res.error) {
message.error(res.error.message)
} else {
mutate(res, 1)
modal.destroy()
}
}
}
const modal = Modal.confirm({
class: 'pure-modal',
width: '500px',
closable: true,
autoFocusButton: null,
title: () => 新建文件夹
,
content: () => (
{useFocus(, true)}
),
onOk: onSave,
})
}
const flashDownload = (i: any) => {
let options = typeof i.hashUpload == 'string' ? { type: i.hashUpload } : i.hashUpload
let params = {
name: '',
hash: '',
size: 0
}
const onSave = async () => {
if (options.size && !params.size) return
if (!params.hash || !params.name) return
let res = await request.fileHashDownload({ id: i.id, ...params })
// console.log(res)
if (res.error) {
message.error(res.error.message)
throw new Error()
} else {
message.success('秒传成功')
// mutate(res)
modal.destroy()
reload()
}
}
const modal = Modal.confirm({
class: 'pure-modal',
width: '500px',
closable: true,
autoFocusButton: null,
title: () => 云端秒传
,
content: () => (
),
onOk: onSave,
})
}
const uploadConfirm = (files: Array, dest: string, id: string) => {
const { create } = useUpload()
let checked: Array = files.map((_, idx: number) => idx)
const onChecked = (ids: Array) => {
checked = ids
}
const ensure = () => {
const checkedFiles = checked.map((idx: number) => files[idx])
create(checkedFiles, 'md5', dest, id)
message.success('任务创建成功')
}
const data = files.map((i, idx) => ({
idx: idx,
id: i.name,
size: i.size,
name: i.name,
ctime: i.lastModified,
ctimeDisplay: time(i.lastModified),
sizeDisplay: byte(i.size),
iconType: getFileType(i.name),
checked: true
}))
Modal.confirm({
class: 'pure-modal',
width: '500px',
closable: true,
title: () => 文件上传
,
content: ,
onOk: ensure,
})
}
const remove = (files: IFile | Array) => {
if (!Array.isArray(files)) {
files = [files]
}
if (files.some(i => i.type == 'drive')) {
removeDisk(files.filter(i => i.type == 'drive'))
return
}
let count = files.length
const error: Array = []
const current = ref(0)
const cancelFlag = ref(false)
const key = '' + Math.random()
const modal = Modal.confirm({
title: '删除文件',
content: `确认删除 ${count == 1 ? files[0].name : `${count} 项`}?`,
onOk() {
execRemove()
},
onCancel() {
cancelFlag.value = true
}
})
const execRemove = async () => {
message.loading({ content: '正在删除', key, duration: 0 });
for (let i of files as Array) {
if (cancelFlag.value) return
message.loading({ content: `[${current.value + 1}/${count}]` + '正在删除:' + i.name, key, duration: 0 });
let res = await request.fileDelete({ id: i.id })
if (res.error) {
error.push(i.id)
} else {
mutate(i, true)
}
current.value++
}
let errorFiles = files.filter((i: IFile) => error.includes(i.id))
if (errorFiles.length) {
return modal.update({
content: () =>
})
} else {
message.success({ content: '已成功删除', key, duration: 1 });
}
}
}
const setDisk = (data: IDrive, idx = -1, msg: string = '修改成功') => {
const updateData = async (modifyData: IDrive) => {
const saveData = [...config.drives]
// console.log(saveData, idx)
if (idx == -1) {
saveData.push(modifyData)
} else {
saveData[idx] = modifyData
}
await setConfig({ drives: saveData }, msg)
// update files
if (!current.path || current.path == '/') {
setPath({ path: '/' }, true)
}
modal.destroy()
}
const modal = Modal.confirm({
class: 'fix-modal--alone',
width: '720px',
closable: true,
content: (
),
onOk: () => { },
})
}
const addDisk = () => {
setDisk(
{
name: '',
protocol: '',
},
-1,
'创建成功'
)
}
const removeDisk = async (files: IFile | Array) => {
if (!Array.isArray(files)) {
files = [files]
}
const execRemove = () => {
const saveData = [...config.drives]
for (let i of files as Array) {
let idx = saveData.findIndex(j => j.id == i.extra?.config_id)
if (idx) {
saveData.splice(idx, 1)
}
}
setConfig({ drives: saveData }, '删除成功')
if (!current.path || current.path == '/') {
setPath({ path: '/' }, true)
}
}
//await request.removeDrive({drive: files.map(i => i.id) })
let count = files.length
Modal.confirm({
title: '删除挂载盘',
content: `确认删除 ${count == 1 ? files[0].name : `${count} 项`}?`,
onOk() {
execRemove()
},
onCancel() {
}
})
}
const remoteDownload = (i: any) => {
let url = ref('')
let threadNum = ref(1)
const onSave = async () => {
if (url.value) {
let res = await request.fileUpdate({ dest: i.id, id: url.value, threadNum: threadNum.value })
if (res.error) {
message.error(res.error.message)
throw new Error()
} else {
message.success('任务创建成功')
modal.destroy()
}
}
}
const modal = Modal.confirm({
class: 'pure-modal',
width: '500px',
closable: true,
autoFocusButton: null,
title: () => 离线下载
,
content: () => (
{
i.remoteDownload != false ?
:
}
{
useFocus(
),
onOk: onSave,
})
}
const showInfo = (file: IFile) => {
const modal = Modal.info({
class: 'pure-modal',
width: '500px',
closable: true,
appContext,
autoFocusButton: null,
// title: 文件
,
content: () => (
),
})
}
return {
rename, move, remove, mkdir, flashDownload, uploadConfirm,
setDisk, addDisk, removeDisk, remoteDownload, showInfo
}
}
================================================
FILE: packages/sharelist-manage/src/views/disk/partial/auth/index.less
================================================
.auth-box{
height:70vh;
width:100%;
display:flex;
align-items:center;
justify-content:center;
flex-direction:column;
box-sizing:border-box;
.auth-box__wrap{
width:320px;
}
.auth-box__header{
font-size:14px;
word-wrap: break-word;
word-break: break-all;
color:var(--primary-text-color);
margin-bottom:24px;
}
.auth-box__input{
padding:8px 11px;
}
.auth-box__btn{
width:100%;
margin-top:24px;
height:42px;
}
}
================================================
FILE: packages/sharelist-manage/src/views/disk/partial/auth/index.tsx
================================================
import { ref, defineComponent, watch, onMounted, toRef, watchEffect, reactive } from 'vue'
import { Button, Input } from 'ant-design-vue'
import useDisk from '../useDisk'
import { LockOutlined } from '@ant-design/icons-vue'
import './index.less'
export default defineComponent({
props: {
message: {
type: String
},
scope: {
type: Object
}
},
emits: ['auth'],
setup(props) {
const { setAuth } = useDisk()
const token = ref('')
const onEnter = () => {
let set: Record = {
token: token.value
}
setAuth({ id: props.scope?.id, path: props.scope?.path, token: token.value })
}
watch(() => props.scope, () => {
token.value = ''
})
return () => (
{
props.scope?.path ? : null
}
(token.value = e.target.value as string)}
placeholder="输入目录访问密码"
onPressEnter={onEnter}
>
{{
prefix: () =>
}}
)
},
})
================================================
FILE: packages/sharelist-manage/src/views/disk/partial/breadcrumb/index.less
================================================
.drive-breadcrumb{
a{
// color:var(--primary-color);
font-weight: 600;
max-width: 100%;
overflow: hidden;
white-space: nowrap;
-o-text-overflow: ellipsis;
text-overflow: ellipsis;
font-size:18px;
}
.ant-breadcrumb{
color:var(--context-3);
a,.ant-breadcrumb-separator{
color:var(--context-3);
}
a:hover{
color:var(--primary-color);
}
}
.ant-breadcrumb > span:last-child a {
color: var(--primary-text-color);
}
&.drive-breadcrumb--sm{
a{font-size: 12px;}
}
}
.drive-breadcrumb-pop-item{
padding:8px 16px;
&:hover{
background-color: var(--primary-hover-bg-color);
transition: all 0.3s;
cursor: pointer;
}
}
.drive-breadcrumb-wrap{
overflow-x: visible;
}
================================================
FILE: packages/sharelist-manage/src/views/disk/partial/breadcrumb/index.tsx
================================================
import { ref, defineComponent, reactive, onMounted, onUnmounted, computed, watchEffect, PropType, watch } from 'vue'
import Icon from '@/components/icon'
import { Breadcrumb, Popover, List } from 'ant-design-vue'
import { EllipsisOutlined } from '@ant-design/icons-vue'
import './index.less'
export default defineComponent({
props: {
paths: {
type: Array as PropType>
},
size: {
type: String,
default: 'default'
}
},
emit: ['tagClick'],
setup(props, ctx) {
const el = ref()
const defaultClietHeight = ref(0)
const lastClientWidth = ref(0)
const onclick = (path: string, idx: number) => {
ctx.emit('tagClick', { path, index: idx })
}
let ellipsisRange = ref(1)
const onUpdate = () => {
let { clientWidth, clientHeight } = el.value
if (defaultClietHeight.value != clientHeight) {
ellipsisRange.value++
lastClientWidth.value = clientWidth
}
}
let observer: ResizeObserver | null
onMounted(() => {
if (el.value) {
if (!defaultClietHeight.value) {
defaultClietHeight.value = el.value.clientHeight
}
observer = new ResizeObserver(entries => {
onUpdate() // entries[0].contentRect
})
observer.observe(el.value)
}
window.addEventListener('resize', onUpdate)
})
onUnmounted(() => {
observer?.disconnect()
observer = null
window.removeEventListener('resize', onUpdate)
})
watch(() => props.paths, (nv, ov) => {
let nvl = nv?.length || 0
let ovl = ov?.length || 0
if (nvl < ovl) {
ellipsisRange.value = 1
}
onUpdate()
})
const createItem = () => {
const nodes = []
const paths = props.paths || []
if (paths.length == 0) return []
nodes.push(
onclick(paths[0], 1)}>
{paths[0]}
)
if (ellipsisRange.value > 1) {
let dataSrc = paths.slice(1, 1 + ellipsisRange.value)
nodes.push(
{{
default: () => ,
content: () => {{
renderItem: ({ item, index }: { item: string, index: number }) => onclick(item, ellipsisRange.value + index)} class="drive-breadcrumb-pop-item">{item}
}}
}}
)
nodes.push(...paths.slice(1 + ellipsisRange.value).map((i, idx) => (
onclick(i, idx + 1 + ellipsisRange.value)}>
{i}
)))
} else {
nodes.push(...paths.slice(1).map((i, idx) => (
onclick(i, idx + 1 + 1)}>
{i}
)))
}
return nodes
}
return () => (
onclick('', 0)}>
文件
{createItem()}
)
},
})
================================================
FILE: packages/sharelist-manage/src/views/disk/partial/error/index.less
================================================
.err{
height:70vh;
width:100%;
display:flex;
align-items:center;
justify-content:center;
flex-direction:column;
box-sizing:border-box;
.err__status{
font-size: 36px;
font-family: inherit;
font-weight: 500;
line-height: 1.1;
color:var(--primary-text-color);
}
.err__msg{
color:var(--context-2);
font-size: 16px;
font-family: 'Source Code Pro','microsoft yahei',Lato,Helvetica,Arial,sans-serif;
}
}
================================================
FILE: packages/sharelist-manage/src/views/disk/partial/error/index.tsx
================================================
import { defineComponent, watch } from 'vue'
import './index.less'
import { message } from 'ant-design-vue'
import AuthBox from '../auth'
export default defineComponent({
props: {
value: {
type: Object,
required: true,
},
},
emits: ['auth'],
setup(props, ctx) {
if (props.value.code == 401 && props.value.message) {
message.error(props.value.message)
}
return () => {
if (props.value.code) {
return props.value.code == 401 ? (
ctx.emit('auth', d)} />
) : (
{props.value.code}
{props.value.message}
)
} else {
return ctx.slots.default?.()
}
}
},
})
================================================
FILE: packages/sharelist-manage/src/views/disk/partial/meta/index.less
================================================
.file-meta{
display: flex;
align-items: center;
.item-thumb{
width:60px;height:40px;
border-radius: 6px;
background-size: cover;
background-position: center center;
background-repeat: no-repeat;
}
.item-icon{
position: relative;
width:60px;
margin-right:8px;
text-align: center;
flex:none;
}
.item-icon--lite{
width:40px;
}
.item-icon__ext{
position: absolute;
font-size: 12px;
bottom: 5px;
color:inherit;
text-transform: uppercase;
transform: scale(0.8);
transform-origin:center;
// width:42px;
text-align: center;
width:100%;
font-weight: bold;
}
.item-desc{
font-size:12px;
color:var(--context-2);
}
.item-dot{
display: inline-block;
padding: 0 7px;
align-items: center;
&:before{
content: "";
display: block;
width: 2px;
height: 2px;
background: var(--primary-text-secondary-color);
}
}
}
================================================
FILE: packages/sharelist-manage/src/views/disk/partial/meta/index.tsx
================================================
import { defineComponent, withModifiers, ref } from "vue";
import Icon from '@/components/icon'
import useDisk from '../useDisk'
import { Spin, Badge } from 'ant-design-vue'
import Error from '../error'
import './index.less'
import Breadcrumb from '../breadcrumb'
export const Meta = defineComponent({
props: {
data: {
type: Object as PropType
},
errorMode: {
type: Boolean
}
},
setup(props) {
return () =>
}
})
export const MetaLite = defineComponent({
props: {
data: {
type: Object as PropType
},
},
setup(props) {
return () => {
let status = props.data?.status
return
}
}
})
export const Tree = defineComponent({
props: {
dirMode: {
type: Boolean
},
treeStyle: {
type: Object
},
excludes: {
type: Array
}
},
emits: ['select'],
setup(props, ctx) {
const routeStacks = ref>([])
const { loading, files, error, setPath, paths, loadMore, diskConfig, id, current, onUpdate } = useDisk({
routeSlient: true,
new: true,
filter: (i: IFile) => {
return (i.type == 'folder' || i.type == 'drive') && !props.excludes?.includes(i.id)
}
})
const onSelect = (data: Partial, append = false) => {
let route: Partial = {}
if (append) {
let lastPath = (routeStacks.value[routeStacks.value.length - 1]?.path || '')
route.id = data.id
route.path = `${lastPath == '/' ? '' : lastPath}` + '/' + data.name
route.name = data.name
routeStacks.value.push(route)
} else {
let idx = routeStacks.value.findIndex((i: IFile) => i.path == data.path)
console.log(idx)
// remote routeStacks
if (idx < routeStacks.value.length - 1) {
route = routeStacks.value[idx]
routeStacks.value.splice(idx + 1)
}
}
setPath(route)
console.log(diskConfig.value)
ctx.emit('select', route, data.config || diskConfig.value)
}
onUpdate(() => {
if (diskConfig.isRoot) {
console.log('disabled')
}
})
const onTagClick = ({ path, index }: any = {}) => {
console.log('tag', index)
onSelect(routeStacks.value[index])
}
onSelect({ path: '', name: '' }, true)
return () => (
)
},
})
================================================
FILE: packages/sharelist-manage/src/views/disk/partial/modifier/index.less
================================================
.modifier{
.modifier__footer{
padding:0 16px;
display: flex;
align-items: center;
justify-content: space-between;
width:100%;
}
}
================================================
FILE: packages/sharelist-manage/src/views/disk/partial/modifier/index.tsx
================================================
import { ref, Ref, defineComponent, watchEffect, toRaw, reactive, UnwrapRef, watch } from 'vue'
import { useSetting } from '@/hooks/useSetting'
import { Switch, Modal, Input, Form, Button, Select, Tooltip } from 'ant-design-vue'
import { useObject } from '@/hooks/useHooks'
import { QuestionCircleOutlined } from '@ant-design/icons-vue'
import './index.less'
const { Item: FormItem, useForm } = Form
type FormState = {
protocol: string
name: string
[key: string]: string | undefined
}
const getFitDriver = (protocol: string | undefined, drivers: Array) => {
const hit = drivers.find((i) => i.protocol == protocol)
if (hit) {
return hit
}
}
const convBoolean = (v: string) => {
return v === 'true' ? true : false
}
const parseFields = (
fields: Array,
formState: FormState,
defaultValues: any,
formItemsNode: any = [],
innerRule: any = [],
): any => {
for (const i of fields) {
formState[i.key] = i.value === undefined ? (i.type == 'boolean' ? convBoolean(defaultValues[i.key]) : defaultValues[i.key]) : i.value
if (i.required) {
innerRule[i.key] = [{ required: true, message: '必填项 / required' }]
}
if (i.options) {
formItemsNode.push(
{{
label: () => {i.label}{i.help ? : null}
,
default: () =>
}}
,
)
if (i.fields) {
const hitVal = formState[i.key]
const hitIndex = i.options?.findIndex((i) => i.value == hitVal) || 0
const hitField = (i.fields[hitIndex] as any) || []
// console.log(i.options, formState, hitVal, hitField, 'hitField')
parseFields(hitField, formState, defaultValues, formItemsNode, innerRule)
}
} else if (i.type == 'boolean') {
formItemsNode.push(
{{
label: () => {i.label}{i.help ? : null}
,
default: () =>
}}
,
)
if (i.fields) {
const isTrue = formState[i.key]
if (isTrue) {
parseFields(i.fields, formState, defaultValues, formItemsNode, innerRule)
}
}
} else if (i.type != 'hidden') {
formItemsNode.push(
{{
label: () => {i.label}{i.help ? : null}
,
default: () =>
}}
,
)
}
}
return [formItemsNode, innerRule]
}
export default defineComponent({
props: {
defaultValue: {
type: Object,
required: true,
},
},
emits: ['update'],
setup(props, ctx) {
const { config } = useSetting()
const formRef = ref()
const drivers: Array = [
...config.drivers,
]
// const [rules, clearRules] = useObject()
const driverTypes = drivers.map((i: Driver) => ({ value: i.protocol, label: i.name }))
const [formState, clearFormState]: [UnwrapRef, any] = useObject({
name: props.defaultValue.name,
protocol: props.defaultValue.protocol,
id: props.defaultValue.id
})
const rules: Ref = ref({})
const formItems: Ref = ref([])
const onSave = () => {
formRef.value
.validate()
.then(() => {
const { name, id, protocol, ...config } = toRaw(formState)
ctx.emit('update', { name, id, protocol, config })
})
.catch((err: any) => {
console.log('error', err)
})
}
watch(
() => formState.protocol,
() => {
clearFormState(['protocol', 'name'])
},
)
watchEffect(() => {
const defaultValues = { ...toRaw(formState), ...props.defaultValue.config }
formRef.value?.clearValidate()
const innerRule = {
name: [{ required: true, message: '必填项 / required' }],
protocol: [{ required: true, message: '必填项 / required' }],
}
const formItemsNode = [
,
,
]
const driver = getFitDriver(formState.protocol, drivers)
if (driver?.guide?.length) {
const [nodes, rules] = parseFields(driver.guide, formState, defaultValues)
if (nodes) {
formItemsNode.push(...nodes)
}
if (rules) {
Object.assign(innerRule, rules)
}
}
formItems.value = formItemsNode
rules.value = innerRule
})
return () => (
)
},
})
================================================
FILE: packages/sharelist-manage/src/views/disk/partial/search/index.less
================================================
.search-box{
position: absolute;
}
================================================
FILE: packages/sharelist-manage/src/views/disk/partial/search/index.tsx
================================================
import { ref, defineComponent, watch, onMounted } from 'vue'
import { InputSearch, RadioGroup, Radio } from 'ant-design-vue'
import './index.less'
import useDisk from '../useDisk'
export default defineComponent({
emits: ['search'],
setup(props, ctx) {
const { diskConfig, setQuery } = useDisk()
const onSearch = (value: string) => {
if (value) {
// router.push({ path: router.currentRoute.value.path, query: { search: value } })
setQuery({ search: value })
ctx.emit('search')
}
}
const options: Array = []
if (diskConfig.globalSearch) {
options.push({ label: '所有文件', value: 'global' })
}
if (diskConfig.localSearch) {
options.push({ label: '当前目录', value: 'local' })
}
const searchType = ref(options[0]?.value)
return () => <>
{options.length ? searchType.value = e.target.value} name="radioGroup" > : null}
>
},
})
================================================
FILE: packages/sharelist-manage/src/views/disk/partial/task/index.less
================================================
.task{
width: 375px;
// background-color:var(--context-background);
border-radius: 4px;
// box-shadow: 0 6px 16px -8px rgb(0 0 0 / 8%), 0 9px 28px 0 rgb(0 0 0 / 5%), 0 12px 48px 16px rgb(0 0 0 / 3%);
.task-list{
max-height: 500px;
overflow-y:auto;
}
.ant-tabs-nav{
margin-bottom:0;
}
.ant-popover-inner-content{
padding:0;
}
.ant-tabs-tab-active{
.anticon {
color:var(--primary-color);
}
}
.ant-list-items{
min-height: 320px;
}
.item{
padding:12px;
font-size:13px;
display: flex;
align-items: center;
width: 100%;
position: relative;
border-bottom: 1px solid var(--divider-3);
.item__body{
flex:auto;
overflow: hidden;
}
.item__head{ margin-right:8px;}
.item__foot{
// position: absolute;
// right: 12px;
}
.item__title{
width:100%;
display: flex;
align-items: center;
justify-content: space-between;
a{
color:var(--primary-text-color);
}
.item__title-link{
color:var(--context-3);
}
}
.item__dot{
display: inline-block;
padding: 0 7px;
align-items: center;
&:before{
content: "";
display: block;
width: 2px;
height: 2px;
background: var(--primary-text-secondary-color);
}
}
.item__progress{
position: absolute;
left:0;
top:0;
height:100%;
transition: all 0.3s;
background-color: var(--primary-progress);
pointer-events:none;
}
.item-action{
margin-left: 16px;
}
.item-meta-description{
font-size:12px;
color:var(--context-3);
margin-top:5px;
}
.action{
display: flex;
visibility: hidden;
}
.action span{
display: flex;
cursor: pointer;
align-items: center;
font-size: 20px;
width: 24px;
height: 24px;
border-radius: 50%;
justify-content: center;
transition: background-color 0.3s;
span:hover{
background-color: #84858d14;
}
}
&:hover .action{
visibility: visible;
}
}
}
.task-error{
height:260px;overflow-y:auto;
.error-item{
padding:6px 0;
font-size:12px;
}
}
================================================
FILE: packages/sharelist-manage/src/views/disk/partial/task/index.tsx
================================================
import { defineComponent, onUnmounted } from "vue"
import { useApi } from '@/hooks/useApi'
import { useRequest } from '@/hooks/useRequest'
import { List, Badge, message, Tabs, Modal, Spin, Alert, Button } from 'ant-design-vue'
import { CloudSyncOutlined, FileSyncOutlined, DeleteOutlined, InfoCircleOutlined, PauseOutlined, ReloadOutlined, DoubleRightOutlined, CloseOutlined, DownloadOutlined } from '@ant-design/icons-vue'
import useDisk from '../useDisk'
import { byte, formatFile } from '@/utils/format'
import { useUpload } from '../upload'
import './index.less'
import { Meta, MetaLite } from '../meta'
export enum STATUS {
INIT = 1, //1 正在生成任务(解析文件)
INIT_ERROR = 2, //2 解析文件过程发生错误
PROGRESS = 3, //3 正在复制
SUCCESS = 4, //4 操作完成
DONE_WITH_ERROR = 5,//5.操作完成 但发生部分完成
ERROR = 6,//6 失败
PAUSE = 7,//已暂停
}
type ITask = {
src: string
dest: string
id: string
// 1 正在生成任务(解析文件),2 解析文件过程发生错误, 3 正在复制,4 操作完成 且未发生错误,5. 操作完成 但发生部分完成
status: STATUS
index: number,
current: string
count: number
size: number
currentLoaded: number
completed: number
message?: string
readCompleted?: number,
speed: number,
error?: Array,
[key: string]: any
}
type TaskSet = {
move: Array,
download: Array,
}
export default defineComponent({
setup(props, ctx) {
let request = useApi()
const { setPath } = useDisk()
const { tasks: uploadTasks, remove: removeUpload, pause: pauseUpload, resume: resumeUpload } = useUpload()
const { data, runAsync, loading, cancel } = useRequest(async () => {
let res = await request.tasks()
return {
move: res.filter((i: ITask) => !i.srcId.startsWith('http')),
download: res.filter((i: ITask) => i.srcId.startsWith('http'))
}
}, {
pollingInterval: 1000,
immediate: true,
loadingDelay: 1000,
pollingWhenHidden: false
})
const navSrc = (i: ITask) => {
if (!i.srcId.startsWith('http')) {
// it's a file
if (i.count == 1) {
setPath({ path: '/' + i.src.split('/').slice(0, -1).join('/') })
} else {
setPath({ path: '/' + i.src })
}
}
}
const navDest = (i: ITask) => {
setPath({ path: '/' + i.dest })
}
const onResume = async (i: ITask) => {
let res = await request.resumeTask(i.id)
if (res.error) {
message.error(res.error.message)
} else {
message.success('操作成功')
}
}
const onPause = async (i: ITask) => {
let res = await request.pauseTask(i.id)
if (res.error) {
message.error(res.error.message)
} else {
message.success('操作成功')
}
}
const retry = async (taskId: string, modal: any) => {
let res = await request.retryTask(taskId)
if (res.error) {
message.error(res.error.message)
} else {
message.success('操作成功')
modal.destroy()
}
}
const onQuery = async (i: ITask) => {
let res = await request.task(i.id)
if (res.error && !res.id) {
message.error(res.error.message)
} else {
let files = res.files || [], error = res.error || [], index = res.index
// 0 waiting 1 progress 2 success 3 fail
files.forEach((i: IFile, idx: number) => {
if (error.includes(idx)) {
i.status = 3
} else {
i.status = idx < index ? 2 : idx > index ? 0 : 1
}
})
useShowFiles(files, i.id, files.some((i: IFile) => i.status == 3))
}
}
const useShowFiles = (files: Array, taskId: string, showRetryButton?: boolean) => {
let data = files.map((i: IFile) => {
i.name = (i.dest ? `${i.dest}/` : '') + i.name
i.type = 'file'
i.ext = i.name.split('.').pop()
formatFile(i)
return i
})
const modal = Modal.confirm({
class: 'pure-modal pure-modal-hide-footer',
width: '500px',
closable: true,
title: () => 任务详情
,
content: (
{
showRetryButton ? : null
}
),
})
}
const queryUpload = async (i: ITask) => {
useShowFiles(i.files.filter((i: IFile) => i.message), i.id, false)
}
onUnmounted(() => {
cancel()
})
let { loading: removeLoading, run: remove } = useRequest(async (task: ITask) => {
let res = await request.removeTask(task.id)
if (res.error) {
message.error(res.error.message)
} else {
await runAsync()
}
})
let { loading: removeDownloadLoading, run: removeDownloadTask } = useRequest(async (task: ITask) => {
let res = await request.removeDownloadTask(task.id)
if (res.error) {
message.error(res.error.message)
} else {
await runAsync()
}
})
//
const createTitle = (i: ITask, mid: string) => {
let src = i.src.split('/').pop()
let dest = i.dest.split('/').pop()
let attrs: Record = {}
if (i.srcId.startsWith('http')) {
attrs.href = i.srcId
attrs.target = '_blank'
}
return
{
i.status == STATUS.PROGRESS && i.count > 1 ?
当前第 {`${i.index + 1} / ${i.count}`} 项 {i.current}
: null
}
{/*
共 {i.count} 项
*/}
}
const renderItem = ({ item: i, index }: { item: ITask, index: number }) => {
let progress = `${Math.floor(100 * i.progress)}%`
return
{
(i.status == STATUS.PROGRESS || i.status == STATUS.PAUSE) ?
: null
}
{/*
*/}
{createTitle(i, '迁移至')}
}
const renderUploadItem = ({ item: i, index }: { item: ITask, index: number }) =>
{
i.status == 3 ?
: null
}
{createTitle(i, '上传至')}
return () => e.stopPropagation()}>
跨盘迁移
}}>
离线下载
}}>
文件上传
}}>
}
})
================================================
FILE: packages/sharelist-manage/src/views/disk/partial/upload/index.tsx
================================================
import { defineComponent } from 'vue'
import { useApi, ReqResponse } from '@/hooks/useApi'
import { reactive, ref, Ref } from 'vue'
import SparkMD5 from 'spark-md5'
import useDisk from '../useDisk'
import { STATUS } from '../task'
import { message } from 'ant-design-vue'
import SHA1 from 'js-sha1'
const parseSize = (v: string) => {
if (!v) return { type: undefined, size: 0 }
let unitMap: Record = { 'k': 1024, 'm': 1024 * 1024 }
let [_, type, num, unit] = v.toLowerCase().match(/(md5|sha1)?\-?(\d+)(k|m)?/i) || []
let size = num ? parseInt(num) : 0
if (unit && unitMap[unit]) {
size *= unitMap[unit]
}
return { type, size }
}
interface readChunkedOptions {
onChunk(...rest: Array): any
onFinish(...rest: Array): any
chunkSize: number
}
function readChunked(file: File, { onChunk, onFinish, chunkSize }: readChunkedOptions) {
const fileSize = file.size
let offset = 0
const reader = new FileReader()
reader.onload = function () {
if (reader.error) {
console.log(reader.error)
onFinish(reader.error || {})
return
}
offset += (reader as any).result.byteLength
// offset += (reader as any).result.length
// return
// callback for handling read chunk
// TODO: handle errors
onChunk(reader.result, offset, fileSize)
if (offset >= fileSize) {
onFinish(null)
return
} else {
readNext()
}
}
reader.onerror = function (err) {
onFinish(err || {})
}
function readNext() {
const fileSlice = file.slice(offset, offset + chunkSize)
reader.readAsArrayBuffer(fileSlice)
}
readNext()
}
//3032ac4e71df951cccff8fdebad5266c
// single hash, head hash, chunk hash
export const getHash = async (file: File, hashtype: string) =>
new Promise((resolve, reject) => {
let parts = hashtype.split('_')
let type = parts[0]
let [headHash, partsHash] = parts.slice(1).map(parseSize)
const hash = type == 'md5' ? new SparkMD5.ArrayBuffer() : type == 'sha1' ? SHA1.create() : null
let defaultChunkSize = 50 * 1024 * 1024
let ret: any = {
head: '',
parts: []
} //Array = []
if (partsHash?.size) {
defaultChunkSize = partsHash.size
}
const onEnd = () => resolve(ret)
if (hash) {
readChunked(file, {
chunkSize: defaultChunkSize,
onChunk(chunk: any, offset: number) {
// content hash update
if (type == 'md5') {
hash.append(chunk)
} else {
hash.update(chunk)
}
// console.log(ret.head, headHash)
// head hash
if (headHash?.size && !ret.head) {
let headHashType = headHash.type || type
if (headHashType) {
ret.head = SparkMD5.ArrayBuffer.hash(chunk.slice(0, headHash.size))
} else if (headHashType == 'sha1') {
ret.head = SHA1(chunk.slice(0, headHash.size))
}
}
// parts hash
if (partsHash?.size) {
let partsHashType = partsHash.type || type
if (partsHashType == 'md5') {
ret.parts.push(SparkMD5.ArrayBuffer.hash(chunk))
} else if (partsHashType == 'sha1') {
ret.parts.push(SHA1(chunk))
}
}
},
onFinish(err: Error) {
if (err) {
reject(err)
} else {
// TODO: Handle errors
// const final = hash.finalize()
// resolve(final.toString(CryptoJS.enc.Hex))
const final = type == 'md5' ? hash.end() : hash.hex()
ret[type] = final
onEnd()
}
}
}
)
}
})
type IUseUpload = {
(): any
instance?: any
}
interface IUseUploadResult {
create(): Promise
}
export const useUpload: IUseUpload = (): IUseUploadResult => {
if (useUpload.instance) {
return useUpload.instance
}
const { reload, current } = useDisk()
const tasks: Ref> = ref([])
// 1 正在生成任务(解析文件/读取文件 .etc),2 解析文件过程发生错误, 3 正在复制,4 操作完成 且未发生错误,5. 操作完成 但发生部分完成
const create = (files: Array, hashType: string, dest: string, id: string, dir: boolean = false) => {
const src = dir ? files[0].webkitRelativePath.split('/')[0] : files[0].name
const size = files.reduce((t, c) => t + c.size, 0)
const task: Record = {
id: '' + Date.now(),
count: files.length,
status: STATUS.INIT,
completed: 0,
currentCompleted: 0,
current: '',
readCompleted: 0,
size,
src,
srcId: 'local:' + src,
dest,
destId: id,
index: 0,
speed: 0,
hashType,
files: files.map(i => ({
name: i.name,
size: i.size,
dest: i.webkitRelativePath.split('/').slice(0, -1).filter(Boolean).join('/'),
file: i
})),
error: []
}
tasks.value.push(task)
createTransferTask(task.id)
}
const createTransferTask = async (taskId: string) => {
let idx = tasks.value.findIndex((i: any) => i.id == taskId)
const request = useApi()
if (tasks.value[idx].status == STATUS.PROGRESS) {
return
}
tasks.value[idx].status = STATUS.PROGRESS
while (tasks.value[idx].index < tasks.value[idx].files.length) {
let task = tasks.value[idx]
let { index, files, hashType, destId } = task
let { file, hash, dest, taskId } = files[index]
if (!hash && hashType) {
hash = await getHash(file, hashType)
console.log('hash:', hash)
files[index].hash = hash
}
const controller = new AbortController()
let lastTime = 0, lastDataCount = 0
let formData: Record = {
id: destId,
size: file.size,
name: file.name,
hash,
hash_type: hashType,
}
tasks.value[idx].cancel = () => controller.abort()
tasks.value[idx].status = STATUS.PROGRESS
try {
//创建/查询任务
let taskData = await request.fileCreateUpload({ ...formData, dest, task_id: taskId })
//快速上传
if (taskData.completed) {
tasks.value[idx].completed += file.size
tasks.value[idx].currentCompleted = 0
tasks.value[idx].index = index + 1
tasks.value[idx].current = file.name
continue
}
//! 任务ID不存在,直接标记失败
if (!taskData.taskId) {
tasks.value[idx].status = STATUS.INIT_ERROR
if (taskData.error) {
tasks.value[idx].message = taskData.error.message
files[index].message = taskData.error.message
}
console.log('here')
throw taskData.error
}
//任务ID可能发生变化(如过期 导致原始的上传实例失效,后端会尝试生成新的上传实例,此时uploadId也会随之变化)
tasks.value[idx].taskId = taskData.taskId
files[index].taskId = taskData.taskId
let uploadFile = file
//续传
if (taskData.start) {
uploadFile = file.slice(taskData.start)
tasks.value[idx].currentCompleted = taskData.start
}
tasks.value[idx].current = file.name
let lastTime: number = Date.now(), lastLoaded = 0
let res = await request.fileUpload({
...formData,
taskId: files[index].taskId,
create: 0,
stream: uploadFile,
slice_size: formData.size - taskData.start,
customRequest: (params: any) => {
params.timeout = 0
params.onUploadProgress = (progressEvent: any) => {
let ts = Date.now()
if (ts - lastTime > 1000) {
tasks.value[idx].speed = (progressEvent.loaded - lastLoaded) * 1000 / (ts - lastTime)
lastLoaded = progressEvent.loaded
lastTime = ts
console.log(tasks.value[idx].speed)
}
tasks.value[idx].currentCompleted = taskData.start + progressEvent.loaded
}
params.signal = controller.signal
//return upload(params)
},
})
if (res.error) {
console.log(res)
//abord
if (res.error.code == 'ERR_CANCELED') {
return
} else {
tasks.value[idx].status = STATUS.INIT_ERROR
}
} else {
tasks.value[idx].status = STATUS.SUCCESS
}
} catch (e) {
console.log('error==>', e)
if (!tasks.value[idx].error.includes(index)) {
tasks.value[idx].error.push(index)
}
}
tasks.value[idx].completed += file.size
tasks.value[idx].currentCompleted = 0
tasks.value[idx].index++
}
message.success(`${tasks.value[idx].src} 已完成`)
//finish
if (tasks.value[idx].index >= tasks.value[idx].files.length) {
tasks.value[idx].status = tasks.value[idx].error.length > 0 ? (tasks.value[idx].error.length == tasks.value[idx].count ? STATUS.ERROR : STATUS.DONE_WITH_ERROR) : STATUS.SUCCESS
// update page
console.log(current?.path, tasks.value[idx])
if (current?.path && current.path == tasks.value[idx].dest) {
reload()
}
return
}
}
const remove = async (task: any) => {
let hit = tasks.value.find((i: any) => i.id == task.id)
if (hit) {
tasks.value.splice(hit, 1)
}
}
const pause = async (task: any) => {
const request = useApi()
let idx = tasks.value.findIndex((i: any) => i.id == task.id)
if (idx == -1) {
message.error('没有此任务')
} else {
const res = await request.fileUploadCancel(tasks.value[idx].taskId)
tasks.value[idx].cancel()
tasks.value[idx].status = STATUS.PAUSE
}
}
const resume = async (task: any) => {
let hit = tasks.value.find((i: any) => i.id == task.id)
if (!hit) {
message.error('没有此任务')
} else {
createTransferTask(hit.id)
}
}
useUpload.instance = {
create,
tasks,
remove,
resume,
pause
}
return useUpload.instance
}
export const Upload = defineComponent({
props: {
type: String,
disabled: {
type: Boolean,
default: false
}
},
setup(props, { slots }) {
const file = ref()
const { diskConfig, paths } = useDisk()
const request = useApi()
const { create } = useUpload()
const onOpenFileDialog = () => {
console.log('is open', props.disabled, file.value)
if (props.disabled) {
return
}
file.value?.click?.()
}
const onChange = async (e: any) => {
let { uploadHash, id } = diskConfig.value
let files = [].slice.call(e.target.files)
let dest = '/' + [...paths.value].join('/')
// console.log(uploadHash)
if (uploadHash) {
// let path = '/' + [...paths.value, file.name].join('/')
// console.log(file, paths)
await create(files, uploadHash, dest, id, props.type == 'dir')
} else {
await create(files, null, dest, id, props.type == 'dir')
}
//clear select
if (file.value) {
file.value.value = ''
}
}
let inputProps: Record = props.type == 'dir' ? { webkitdirectory: true, directory: true, multiple: true } : {}
return () =>
e.stopPropagation()} capture={true} style="display:none;" />
{slots.default?.()}
}
})
================================================
FILE: packages/sharelist-manage/src/views/disk/partial/useDisk.ts
================================================
import { useApi, ReqResponse } from '@/hooks/useApi'
import { ref, Ref, watch, reactive, computed, inject, provide, getCurrentInstance, InjectionKey } from 'vue'
import { byte, getFileType, time, formatFile } from '@/utils/format'
import { useLocalStorageState } from '@/hooks/useLocalStorage'
import { message } from 'ant-design-vue'
import { useRouter, useRoute, onBeforeRouteUpdate } from 'vue-router'
interface Handler {
(): any
}
const useFolderAuth = () => {
const data = useLocalStorageState>('auth', {})
const hasAuth = (path?: string) => path && !!data.value[path]
const addAuth = (path: string, v: any) => {
data.value[path] = v
}
const getAuth = (path?: string) => {
return path ? data.value[path] : undefined
}
const removeAuth = (path: string) => {
delete data.value[path]
}
//create a auth chain.
const geneAuth = (path = '') => {
const paths = path.split('/')
const ret: Record = {}
for (let i = 0; i < paths.length; i++) {
const cur = paths.slice(0, i + 1).join('/')
if (data.value[cur]) {
const hit = data.value[cur]
ret[hit[0]] = hit[1]
}
}
return ret
}
return { hasAuth, addAuth, getAuth, geneAuth, removeAuth }
}
type IUseDiskOption = {
id?: string
new?: boolean
routeSlient?: boolean
base?: string
filter?: (i: IFile) => boolean
}
type IUseDisk = {
(options?: IUseDiskOption): any
[key: string]: any
}
type IQuery = {
id?: string
name?: string
path?: string
search?: string
orderBy?: string
auth?: Record
}
type DiskState = {
id?: string
path?: string
search?: string
nextPage?: string
orderBy?: string
}
type IUseDiskAction = any
export const diskSymbol = Symbol() as InjectionKey
const useDisk: IUseDisk = (diskOptions: IUseDiskOption = { base: '/drive/folder' }): any => {
if (inject(diskSymbol)) {
return inject(diskSymbol)
}
const request = useApi()
const router = useRouter()
const files: Ref> = ref([])
const loading = ref(false)
const error = reactive({ code: 0, message: '', scope: {} })
const diskConfig: Ref = ref({})
const current = reactive({ id: undefined, path: undefined, search: '', nextPage: '', orderBy: '' })
const sortOption = useLocalStorageState>('ordeyBy', { key: 'name', type: 'asc' })
const { addAuth, removeAuth, geneAuth } = useFolderAuth()
const basePath = diskOptions.base || ''
const paths = computed(() => {
const ret: Array = current.path?.substring(1).split('/').filter(Boolean) || []
if (current.search) {
ret.push(`${current.search} 的搜索结果`)
}
return ret
})
let controller: AbortController
const getFiles = async (options: IQuery = {}, clear = false): Promise => {
const stateChange = options.path != current.path || !!options.search || (!options.search && current.search)
loading.value = true
const params: Record = {
id: options.id,
path: options.path,
order_by: sortOption.value.key + ' ' + sortOption.value.type,
}
const auth: Record = geneAuth(options.path)
if (options.auth) {
auth[options.auth.id] = options.auth.token
}
if (options.search) {
params.search = options.search
}
params.auth = auth
if (current.nextPage) {
params.next_page = current.nextPage
}
const usePage = !!params.next_page
controller = new AbortController()
params.customRequest = (p: any) => {
p.signal = controller.signal
}
const res: ReqResponse = await request.files(params)
if (res.error) {
error.code = res.error.code
error.message = res.error.message as string
//校验
if (error.code == 401) {
if (error.message) {
message.error(error.message)
}
if (params.auth) {
removeAuth(params.path)
}
if (res.error?.scope) {
//多重目录校验
if (res.error.scope.id != options.id) {
const cur: ReqResponse = await request.filePath({ id: res.error.scope.id })
const path = cur.map((i: IFile) => i.name).join('/')
error.scope = { ...res.error.scope, path }
} else {
error.scope = { id: options.id, path: options.path }
}
// 目录校验通过 需要保存
if (options.auth && options.auth.id != res.error.scope.id) {
addAuth('/' + options.auth.path, [options.auth.id, options.auth.token])
}
current.id = options.id
current.path = options.path
}
} else {
current.path = options.path
current.id = options.id
error.message = res.error.message || ''
}
} else {
if (res.files) {
formatFile(res.files)
if (clear) {
files.value = []
}
if (params.next_page) {
let appendData = res.files as Array
if (diskOptions?.filter) {
appendData = appendData.filter(diskOptions?.filter)
}
files.value.push(...appendData)
} else {
files.value = diskOptions?.filter ? res.files.filter(diskOptions?.filter) : res.files
}
}
current.nextPage = res.nextPage
diskConfig.value = Object.assign(res.config || {}, { id: res.id })
error.code = 0
error.message = ''
// save/update
if (options.auth) {
addAuth('/' + options.auth.path, [options.auth.id, options.auth.token])
}
}
current.search = options.search || undefined
current.id = options.id
current.path = options.path
if (diskOptions?.routeSlient !== true && !usePage && stateChange) {
const target = options.path || ''
let url = (basePath + target).replace(/\/+/g, '/')
if (current.search) {
url += '?search=' + current.search
// only support global search
if (diskConfig.value.search == 1) {
url = basePath + '/' + diskConfig.value.drive + '?search=' + current.search
current.path = '/' + diskConfig.value.drive
}
}
router.push(url)
}
loading.value = false
updateHandlers.forEach((cb: Handler) => cb())
}
const setPath = async ({ ...options }: IQuery = {}, reload = false, next = false) => {
if (options.path) {
options.path = options.path.replace(/\/{2,}/g, '/')
}
const isSameQuery = options.path == current.path && options.search == current.search
if (isSameQuery && !options.auth && !reload) return
if (options.search) {
current.nextPage = undefined
}
controller?.abort()
loading.value = true
if (options.path != current.path) current.nextPage = ''
if (options.id && !options.path) {
const resp = await request.filePath({ id: options.id })
options.path = '/' + resp.map((i: IFile) => i.name).join('/')
}
getFiles(options, true)
}
const setAuth = (auth: Record) => {
setPath({ auth, id: current.id, path: current.path })
}
const setSort = (key: string) => {
if (key === sortOption.value.key) {
sortOption.value.type = sortOption.value.type == 'asc' ? 'desc' : 'asc'
} else {
sortOption.value.key = key
sortOption.value.type = 'asc'
}
//全部加载完毕时
setPath({ id: current.id, path: current.path, search: current.search }, true)
}
const loadMore = () => {
console.log('====> load more', current.id, current.path)
if (loading.value || !current.nextPage) {
return
}
getFiles({
id: current.id,
path: current.path,
search: current.search,
})
}
const reload = () => {
setPath({ id: current.id, path: current.path, search: current.search }, true)
}
const mutate = (file: IFile, isRemove = false) => {
const idx = files.value.findIndex((i) => i.id == file.id)
if (idx >= 0) {
if (isRemove) {
files.value.splice(idx, 1)
} else {
files.value.splice(idx, 1, file)
}
} else {
files.value.unshift(formatFile(file))
}
}
const updateHandlers: Array = []
const onUpdate = (cb: Handler) => {
const cancel = () => {
const idx = updateHandlers.indexOf(cb)
updateHandlers.splice(idx, 1)
}
updateHandlers.push(cb)
return cancel
}
const instance = {
setPath,
files,
paths,
loading,
error,
reload,
loadMore,
diskConfig,
mutate,
setAuth,
current,
setSort,
sortConfig: sortOption,
onUpdate,
}
provide(diskSymbol, instance)
return instance
}
export default useDisk
================================================
FILE: packages/sharelist-manage/src/views/general/index.less
================================================
.page--setting {
// max-width:960px;
padding:32px;
.setting-drive__header {
text-align: right;
}
.item {
border-bottom: 1px solid #e8e8e8;
padding: 16px 8px;
display: flex;
align-items: center;
&:hover{
background-color: var(--context-hover);
}
.item__header {
flex: 1 1 auto;
display: flex;
align-items: center;
}
.item__icon {
margin-right: 8px;
}
.item__meta {
flex: 1 1 auto;
}
.item__meta-title {
color: var(--primary-text-color);
font-size: 14px;
line-height: 22px;
margin-bottom: 0;
}
.item__meta-desc {
color: var(--context-2);
font-size: 14px;
line-height: 22px;
margin-top: 4px;
a{
color:rgba(0,0,0,.85);
padding:0 8px;
}
}
.item-action a {
padding: 0 8px;
}
&.item--plugin{
.item__meta-desc {
color: rgba(0, 0, 0, .8);
font-size: 12px;
line-height: 18px;
}
}
}
}
================================================
FILE: packages/sharelist-manage/src/views/general/index.tsx
================================================
import { ref, defineComponent, watch, onMounted, toRef, toRefs, reactive } from 'vue'
import { RadioGroup, Radio, message, Tooltip, Textarea } from 'ant-design-vue'
import { SaveOutlined, ImportOutlined, QuestionCircleOutlined, LoadingOutlined } from '@ant-design/icons-vue'
import { useSetting, ConfigFieldItem } from '@/hooks/useSetting'
import { Switch, Modal, Input, InputNumber, Alert, Tabs } from 'ant-design-vue'
import './index.less'
const valueDisplay = (value: any, type: string) => {
if (type == 'boolean') {
return Boolean(value) ? '启用' : '禁用'
}
else if (type == 'array') {
const len = value.length
const nodes = value.slice(0, 3).map((i: string) => {i}
)
if (len > 3) {
nodes.push(等{len}项
)
}
return nodes
}
else if (type == 'textarea') {
return value ? '已设置' : '未设置'
} else {
return value
}
}
const StateLabel = defineComponent({
props: {
loading: {
type: Boolean
}
},
setup(props, { slots }) {
return () => props.loading ? : slots.default?.()
}
})
export default defineComponent({
setup() {
const { config, setConfig, exportConfig, configFields } = useSetting()
const state: Record = reactive({})
const readFile = (e: any) => {
var reader = new FileReader(); //这是核心,读取操作就是由它完成.
reader.readAsText(e.target.files[0]); //读取文件的内容,也可以读取文件的URL
reader.onload = function () {
//当读取完成后回调这个函数,然后此时文件的内容存储到了result中,直接操作即可
try {
let data = JSON.parse(reader.result as string)
setConfig(data)
} catch (e) {
message.error('无法读取到配置信息')
}
}
}
const saveConfig = (code: string, val: any) => {
state[code] = true
setConfig({ [code]: val }).then(() => {
delete state[code]
})
}
const createInputModifier = ({ label, code, secret, type, help, handler, validator }: ConfigFieldItem) => {
const modifier = ref(secret ? '' : config[code])
const handleChange = (e: any) => modifier.value = e.target.value
const handleChangeValue = (e: unknown) => modifier.value = e as number
let lastVal = modifier.value
//modal 下的input v-model 有bug
Modal.confirm({
title: label,
class: 'pure-modal',
content: (
{
help ?
: null
}
{
type == 'number' ?
:
}
),
onOk: () => {
if (validator) {
if (!validator(modifier.value, lastVal)) {
message.error('不符合要求')
return Promise.reject()
}
}
saveConfig(code, modifier.value)
handler?.(modifier.value, lastVal)
},
})
}
const createListModifier = (label: string, code: string) => {
const modifier = ref(config[code].join('\n'))
const handleChange = (e: any) => modifier.value = e.target.value
Modal.confirm({
title: label,
class: 'pure-modal',
content: (
),
onOk: () => {
saveConfig(code, modifier.value.split('\n').filter(Boolean))
},
})
}
const createOptionModifier = (label: string, code: string) => {
console.log(label, code, config[code])
const modifier = ref(config[code])
const handleChange = (e: any) => {
modifier.value = e.target.value
}
const options = config[`${code}_options`]
Modal.confirm({
title: label,
class: 'pure-modal',
content: () => (
{
options.map((i: any) => {i})
}
),
onOk: () => {
saveConfig(code, modifier.value)
},
})
}
return () => (
{
configFields.value.map((i, idx) =>
{{
default: () => i.children.map((i) => (
)),
tab: () => (
{i.title}
),
}}
)
}
)
},
})
================================================
FILE: packages/sharelist-manage/src/views/home/index.less
================================================
.layout{
display: flex;
height: 100vh;
// background: var(--background_secondary);
// padding:30px;
// border-radius: 8px;
// box-sizing: border-box;
// background: rgb(222,225,231);
// overflow:hidden;
.layout__sider{
border-right:1px solid var(--divider-2);
}
.layout__content{
flex:1 1 auto;
overflow-y: auto;
// padding:32px 0;
}
}
================================================
FILE: packages/sharelist-manage/src/views/home/index.tsx
================================================
import Sider from '../../components/sider'
import { RouterView } from 'vue-router'
import { defineComponent } from 'vue'
import { useSetting } from '@/hooks/useSetting'
import Signin from '../signin'
import './index.less'
import MediaPlayer, { usePlayer } from '@/components/player'
export default defineComponent({
setup() {
const { loginState } = useSetting()
const { id } = usePlayer('player')
return () => (
loginState.value == 1 ?
: loginState.value == 2 ?
: null
)
}
})
================================================
FILE: packages/sharelist-manage/src/views/plugin/index.less
================================================
.page--plugin{
padding:32px;
.page-content{
display: grid;
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
grid-gap: 16px;
}
.item {
// border-bottom: 1px solid var(--divider-3);
// background:#fafcfe;
padding: 16px;
display: flex;
// align-items: center;
transition: all 0.25s;
&:hover{
background-color: var(--context-hover);
}
.item__header {
flex: 1 1 auto;
display: flex;
// align-items: center;
}
.item__icon {
width:42px;height:42px;
background-repeat: no-repeat;
background-size: contain;
background-position: center center;
margin-right: 12px;
color:var(--context-2);
text-align: center;
flex:none;
}
.item__meta {
flex: 1 1 auto;
}
.item__meta-head{
color:var(--context-1);
font-size: 12px;
line-height: 22px;
}
.item__meta-title {
color: var(--primary-text-color);
font-size: 14px;
line-height: 22px;
margin-bottom: 0;
font-weight: 600;
}
.item__meta-desc {
color:var(--context-2);
font-size: 12px;
line-height: 1.6em;
margin-top: 4px;
a{
color: var(--primary-text-color);
padding-right:8px;
}
}
.item-action{
opacity: 0;
transition: all 0.3s;
flex:none;
}
&:hover,&--aciton-visible{
.item-action{
opacity: 1;
}
}
.item-action a {
padding: 0 8px;
}
}
}
================================================
FILE: packages/sharelist-manage/src/views/plugin/index.tsx
================================================
import { ref, defineComponent, watch, onMounted, toRef, toRefs, reactive, watchEffect } from 'vue'
import Icon from '@/components/icon'
import { useSetting } from '@/hooks/useSetting'
import { Button, Modal, Popconfirm, Tooltip, Dropdown, Menu, Empty } from 'ant-design-vue'
import { PlusOutlined, DeleteOutlined, EditOutlined, LoadingOutlined, HomeOutlined, SyncOutlined, AppstoreOutlined, EllipsisOutlined } from '@ant-design/icons-vue'
import CodeEditor from '@/components/code-editor'
import './index.less'
import Store from './partial/store'
const defaultNewPlugin = `//===Sharelist===
// @name 插件名 e.g. NewSharelistPlugin
// @namespace 命名空间 用于区分插件。e.g. https://new.sharelist.plugin
// @version 版本号 e.g. 1.0.0
// @license 协议 e.g. MIT
// @description 描述
// @author 作者
// @supportURL 插件主页
// @updateURL 插件更新地址
// @icon 插件图标URL 支持base64
//===/Sharelist==`
export default defineComponent({
setup() {
const { config, getPlugin, setPlugin, removePlugin, upgradePlugin } = useSetting()
const update = (data: IPlugin) => {
let newData = ''
const show = (input: string) => {
Modal.confirm({
class: 'pure-modal',
title: data.name,
width: '720px',
closable: true,
content: (
newData = data} />
),
onOk: () => setPlugin(data.id, newData),
})
}
if (data.id) {
getPlugin(data.id).then((res: any) => {
show(res)
})
} else {
show(defaultNewPlugin)
}
}
const remove = (data: IPlugin, idx: number) => {
Modal.confirm({
title: '移除插件',
content: `确认删除 ${data.name}?`,
onOk() {
removing[data.id] = true
removePlugin(data.id).then(() => {
delete removing[data.id]
})
},
onCancel() {
}
})
}
const installing: Record = reactive({})
const removing: Record = reactive({})
const upgrade = (data: IPlugin) => {
installing[data.id] = true
upgradePlugin(data.id).then((res: any) => {
delete installing[data.id]
})
}
const onAction = ({ key }: { key: any }) => {
if (key == 'store') {
const modal = Modal.confirm({
class: 'pure-modal pure-modal-hide-footer',
width: '890px',
closable: true,
title: () => ,
maskClosable: false,
content:
})
} else {
update({ id: '', name: '新建插件' })
}
}
return () => (
{config.plugins.length == 0 ?
:
{config.plugins?.map((i: IPlugin, idx: number) => (
))}
}
)
},
})
================================================
FILE: packages/sharelist-manage/src/views/plugin/partial/store/index.less
================================================
.plugin-store{
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
grid-gap: 12px;
height: 50vh;
overflow-y: auto;
.plugin-item{
display: flex;
padding:8px;
// justify-content: sp;
align-items: center;
}
.plugin-item__install{
font-size:12px;
padding:6px 0;
background:var(--background-cover);
color:var(--primary-text-color);
font-weight: 600;
border-radius: 3px;
width:90px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
&.plugin-item__install--checked,&:hover{
background:var(--primary-color-bg);
color:var(--primary-color);
}
}
.plugin-item-content{
flex:1 1 auto;
}
.plugin-item-icon{
width:42px;height:42px;
background-repeat: no-repeat;
background-size: contain;
background-position: center center;
margin-right: 12px;
color:var(--context-2);
text-align: center;
}
.item-hd{
font-size:15px;
color:var(--primary-text-color);
}
.plugin-item__name{
color:var(--primary-text-color);
font-weight: 600;
font-size:15px;
margin:0.8em 0;
}
.item-bd{
color:var(--context-2);
font-size:12px;
margin-bottom:8px;
}
.item-ft{
display: flex;
font-size:12px;
line-height: 1.6em;
a{
color:var(--context-2);
margin-right:6px;
}
}
}
================================================
FILE: packages/sharelist-manage/src/views/plugin/partial/store/index.tsx
================================================
import { ref, defineComponent, watch, onMounted, toRef, toRefs, reactive, watchEffect, Ref, computed } from 'vue'
import Icon from '@/components/icon'
import { useSetting } from '@/hooks/useSetting'
import { Button, Modal, Popconfirm, Tooltip, Dropdown, Menu, message, Spin } from 'ant-design-vue'
import { PlusOutlined, DeleteOutlined, EditOutlined, LoadingOutlined, HomeOutlined, SyncOutlined, AppstoreOutlined, CheckOutlined } from '@ant-design/icons-vue'
import CodeEditor from '@/components/code-editor'
import './index.less'
import { useApi } from "@/hooks/useApi";
import { useRequest } from '@/hooks/useRequest'
export default defineComponent({
setup() {
const request = useApi()
const { config } = useSetting()
const installingList: Record = reactive({})
const { reloadConfig } = useSetting()
let { loading, run, data } = useRequest(async () => {
let res = await request.pluginStore()
if (res.error) {
message.error(res.error.message)
} else {
console.log(res)
return res
}
}, { immediate: true })
let { loading: installing, run: install } = useRequest(async (i) => {
installingList[i.namespace] = true
let res = await request.installPlugin({ url: i.updateURL })
delete installingList[i.namespace]
if (res.error) {
message.error(res.error.message)
} else {
message.success('安装成功')
reloadConfig()
}
})
const installed = computed(() => {
const ret: Record = {}
config.plugins.forEach((i: any) => {
ret[i.namespace] = true
})
return ret
})
return () =>
{
data?.value?.map((i: any) =>
{
i.icon ?
:
}
{
installed.value[i.namespace] ?
已安装
:
installingList[i.namespace] ?
安装中
:
install(i)}>安装
}
{i.supportURL && (i.supportURL as string).startsWith('http') ? : null}
{i.license ? -
{i.license}
: null}
{i.version ? - v{i.version}
: null}
)
}
}
})
================================================
FILE: packages/sharelist-manage/src/views/signin/index.less
================================================
.page-signin {
.page-signin-wrap{
position: fixed;
top: 45%;
left: 50%;
transform: translate(-50%, -50%);
width: 320px;
}
.page-logo{
position: absolute;
top:24px;left:24px;
font-size: 18px;
color:var(--primary-text-color);
font-weight: 600;
display: flex;
align-items: center;
line-height: 1em;
// &:before{
// content:'';
// width:10px;height: 10px;
// background: var(--primary-text-color);
// border-radius: 5px;
// display: block;
// margin-right:5px
// }
}
.page-signin__input {
padding: 8px 11px;
}
.page-signin__btn {
width: 100%;
margin-top: 36px;
height: 42px;
}
.page-header{
font-size: 18px;
color:var(--primary-text-color);
text-align:center;
margin-bottom: 32px;
font-weight: 600;
}
}
================================================
FILE: packages/sharelist-manage/src/views/signin/index.tsx
================================================
import { ref, defineComponent } from 'vue'
import { Button, Input } from 'ant-design-vue'
import { useSetting } from '@/hooks/useSetting'
import './index.less'
export default defineComponent({
setup() {
const { getConfig } = useSetting()
const token = ref('')
const onEnter = () => {
getConfig(token.value)
}
return () => (
)
},
})
================================================
FILE: packages/sharelist-manage/src/views/test/index.vue
================================================
Home
================================================
FILE: packages/sharelist-manage/tsconfig.json
================================================
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"importHelpers": true,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"baseUrl": ".",
"experimentalDecorators": true,
"noImplicitThis": true,
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
],
"types": [
// "vite/client"
],
"paths": {
"@/*": [
"src/*"
]
},
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
}
================================================
FILE: packages/sharelist-manage/vite.config.ts
================================================
import { defineConfig } from 'vite'
import vueJsx from '@vitejs/plugin-vue-jsx'
import legacy from '@vitejs/plugin-legacy'
import path from 'path'
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
import Components from 'unplugin-vue-components/vite'
const root = path.resolve(__dirname, './src')
export default defineConfig({
root,
base: '',
resolve: {
alias: [{ find: '@', replacement: root }],
extensions: ['.js', '.ts', '.jsx', '.tsx', '.vue', '.json', '.less', '.css'],
},
build: {
outDir: path.join(__dirname, 'dist'),
sourcemap: false,
emptyOutDir: true,
assetsDir: '',
minify: 'esbuild',
reportCompressedSize: false,
},
css: {
preprocessorOptions: {
less: {
javascriptEnabled: true,
modifyVars: {
'preprocess-custom-color': 'green',
},
},
},
},
server: {
port: +process.env.PORT || 3000,
proxy: {
'/api': {
target: 'http://127.0.0.1:33001/',
changeOrigin: true,
},
},
},
plugins: [
vueJsx(),
// legacy({
// targets: ['defaults', 'not IE 11'],
// }),
Components({
resolvers: [
AntDesignVueResolver({
importStyle: true,
}),
],
}),
],
optimizeDeps: {
exclude: [],
},
})
================================================
FILE: packages/sharelist-web/.eslintrc.js
================================================
module.exports = {
parser: 'vue-eslint-parser',
parserOptions: {
// set script parser
parser: '@typescript-eslint/parser', // Specifies the ESLint parser
ecmaVersion: 2021, // Allows for the parsing of modern ECMAScript features
sourceType: 'module', // Allows for the use of imports
ecmaFeatures: {
jsx: true, // Allows for the parsing of JSX
},
validate: [],
},
extends: [
'plugin:vue/vue3-recommended',
'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
'plugin:prettier/recommended',
],
rules: {
// Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
// e.g. "@typescript-eslint/explicit-function-return-type": "off",
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-empty-function': 'off',
// 'object-curly-spacing': ['error', 'always'],
},
}
================================================
FILE: packages/sharelist-web/.gitignore
================================================
node_modules
.DS_Store
dist
dist-ssr
*.local
yarn-error.log
package-lock.json
yarn.lock
================================================
FILE: packages/sharelist-web/.prettierrc.js
================================================
// https://prettier.io/docs/en/configuration.html
module.exports = {
//分号终止符
semi: false,
//行尾逗号
trailingComma: "all",
// 使用单引号, 默认false(在jsx中配置无效, 默认都是双引号)
singleQuote: true,
printWidth: 120,
// 换行符
endOfLine: "auto",
//缩进 default:2
tabWidth: 2
}
================================================
FILE: packages/sharelist-web/CHANGELOG.md
================================================
================================================
FILE: packages/sharelist-web/README.md
================================================
# @sharelist/web [](https://npmjs.com/package/@sharelist/web)
It's a part for sharelist
## Useage
================================================
FILE: packages/sharelist-web/package.json
================================================
{
"name": "@sharelist/web",
"version": "0.2.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s --commit-path .",
"release": "node ../../scripts/release.js --skipBuild --skipNpmPublish"
},
"dependencies": {
"@ant-design/icons-vue": "^6.0.1",
"ant-design-vue": "3.x",
"axios": "^0.21.0",
"pinia": "^2.0.18",
"pinia-plugin-persist": "^1.0.0",
"plyr": "^3.6.8",
"vue": "3.x",
"vue-router": "4.x"
},
"devDependencies": {
"@types/node": "^15.9.0",
"@typescript-eslint/eslint-plugin": "^4.23.0",
"@typescript-eslint/parser": "^4.23.0",
"@vitejs/plugin-legacy": "^2.x",
"@vitejs/plugin-vue": "^2.x",
"@vitejs/plugin-vue-jsx": "^2.x",
"@vue/compiler-sfc": "^3.0.5",
"eslint": "^7.26.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-vue": "^7.9.0",
"less": "^4.1.1",
"minimist": "^1.2.5",
"prettier": "^2.3.0",
"typescript": "^4.1.3",
"vite": "3.x",
"vue-eslint-parser": "^7.6.0",
"vue-tsc": "^0.0.24"
}
}
================================================
FILE: packages/sharelist-web/src/App.tsx
================================================
import { defineComponent } from 'vue'
import { ConfigProvider } from 'ant-design-vue'
import { RouterView } from 'vue-router'
import zhCN from 'ant-design-vue/es/locale/zh_CN';
export default defineComponent({
setup() {
return () => (
)
},
})
================================================
FILE: packages/sharelist-web/src/assets/style/index.less
================================================
// @import './icon.less';
@import './var.less';
@import 'ant-design-vue/lib/style/variable.less';
@import 'ant-design-vue/lib/button/style/index-pure.less';
@import 'ant-design-vue/lib/radio/style/index-pure.less';
@import 'ant-design-vue/lib/breadcrumb/style/index-pure.less';
@import 'ant-design-vue/lib/input/style/index-pure.less';
@import 'ant-design-vue/lib/checkbox/style/index-pure.less';
@import 'ant-design-vue/lib/message/style/index-pure.less';
@import 'ant-design-vue/lib/modal/style/index-pure.less';
@import 'ant-design-vue/lib/spin/style/index-pure.less';
@import 'ant-design-vue/lib/empty/style/index-pure.less';
@import 'ant-design-vue/lib/popover/style/index-pure.less';
@import 'ant-design-vue/lib/alert/style/index-pure.less';
@import 'ant-design-vue/lib/tooltip/style/index-pure.less';
@import 'ant-design-vue/lib/image/style/index-pure.less';
@import 'ant-design-vue/lib/radio/style/index-pure.less';
@import 'ant-design-vue/lib/dropdown/style/index-pure.less';
@font-face {
font-family: 'Source Code Pro';
font-style: normal;
font-weight: 400;
src: local('Source Code Pro Regular'), local('SourceCodePro-Regular'), url(data:application/x-font-woff2;charset=utf-8;base64,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) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
body{
-webkit-app-region: drag;
// padding:5px;
background: var(--background);
color:var(--primary-text-color);
font-size:14px;
font-family: 'Source Code Pro';
}
h1,h2,h3,h4,h5,h6{
color:var(--primary-text-color);
}
ul,ol{
padding-left:0;
}
.no-drag,input,button{
-webkit-app-region: no-drag;
}
.flex{
display: flex;
align-items: center;
&.flex--between{
justify-content: space-between;
}
&.flex--block{
width:100%;
}
}
::-webkit-scrollbar-track {
// box-shadow: inset 0 0 1px rgba(0,0,0,0.2);
background-color: #eee;
}
::-webkit-scrollbar {
width: 10px;
// background-color: rgba(200,200,200,1);
}
::-webkit-scrollbar-thumb {
background-color: rgba(200,200,200,.6);
}
.pure-modal{
.ant-modal-content{ border-radius: 5px;}
.ant-modal-body{ padding:20px;}
.anticon-exclamation-circle{ display: none;}
.ant-modal-confirm-title{
// border-bottom:1px solid #ddd;
padding-bottom: 12px;
// width: fit-content
}
.ant-modal-confirm-content{
margin-left:0 !important;
}
.modal-header{
padding:16px;
}
.modal-footer{
margin-top:16px;
text-align: right;
}
&.pure-modal-hide-footer{
.ant-modal-confirm-btns{
display: none;
}
}
}
.hide-modal{
display: none;
}
.fix-modal--alone{
.ant-modal-body{ padding:16px;}
.ant-modal-confirm-btns,.anticon-exclamation-circle{ display: none;}
.ant-modal-confirm-content{
margin-top: 0;
}
.ant-modal-content{
border-radius: 6px;
}
}
.fix-form--inline{
display: flex;
flex-wrap: wrap;
.ant-form-item{
flex:0 0 50%;padding:0 16px;
&.fix-form-item--foot{
flex:100%;
margin-bottom: 0;
}
}
}
.ant-dropdown-menu{
background-color:var(--context-background);
.ant-dropdown-menu-item:hover, .ant-dropdown-menu-submenu-title:hover{
background-color: var(--context-hover);
}
.ant-dropdown-menu-item-group-title{
color: var(--context-3);
}
}
.ant-checkbox-inner{
background-color:var(--context-background);
}
.ant-select-dropdown{
background-color:var(--context-background);
.ant-select-item{
color:var(--primary-text-color);
}
.ant-select-item-option-active:not(.ant-select-item-option-disabled){
background-color: var(--context-hover);
}
}
.ant-select{
color:var(--primary-text-color);
}
.ant-select:not(.ant-select-customize-input) .ant-select-selector{
border-color:var(--divider-3);
background-color:var(--divider-3);
}
.ant-alert-info{
border-color:var(--primary-color);
background-color:var(--primary-color-bg);
border-radius: 8px;
.ant-alert-message{
// color:var(--primary-color);
color: var(--context-3);
}
}
.ant-popover-inner,.ant-popover-arrow-content,.ant-modal-content{
background-color:var(--context-background);
}
.ant-tabs,.ant-modal-confirm-body .ant-modal-confirm-title,.ant-modal-confirm-body .ant-modal-confirm-content{
color:var(--primary-text-color);
}
.ant-tabs-top>.ant-tabs-nav:before{
border-color:var(--divider-2);
}
.ant-list-empty-text,.ant-empty-normal{
color:var(--primary-text-color);
}
.ant-badge,.ant-radio-wrapper{
color:var(--context-1);
}
.ant-switch{
background-color:var(--context-3);
}
.ant-switch-checked {
background-color: var(--ant-primary-color);
}
.ant-input,.ant-input-number{
background-color:var(--divider-3);
border-color:var(--divider-2);
color:var(--primary-text-color);
}
.ant-input-affix-wrapper{
background-color:var(--divider-3);
border-color:var(--divider-2);
color:var(--primary-text-color);
.ant-input{
background-color: transparent;
}
.ant-input-suffix,.ant-input-password-icon{
color:var(--primary-text-color);
}
}
.ant-message-notice-content{
background-color:var(--divider-3);
color:var(--primary-text-color);
}
.ant-form-item-label>label{
color:var(--primary-text-color);
}
.ant-progress-text{
color:var(--primary-text-color);
}
.sl-input{
border-radius: 6px;
box-shadow: 0 1px 5px rgb(0 0 0 / 12%);
border-color:transparent;
}
.popover-padding-0{
.ant-popover-inner-content{ padding:0;}
}
.ellipsis-2{
overflow : hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
text-overflow: ellipsis;
word-break: break-all;
}
.ellipsis-1{
overflow : hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
text-overflow: ellipsis;
word-break: break-all;
}
.ellipsis{
overflow:hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.danger-aciton{
color:var(--danger-color);
}
.danger-aciton--hover:hover{
color:var(--danger-color) !important;
}
.ant-spin{
max-height: 100vh;
}
.menu-style{
width:160px;
border-radius: 5px;
.menu-item{
padding-top:8px;
padding-bottom:8px;
color:var(--primary-text-color);
}
.ant-dropdown-menu{
border-radius: 5px;
}
.ant-dropdown-menu-item-group-list{
margin:0;
}
}
.popover-padding-0{
.ant-popover-inner-content{ padding:0;}
}
@media screen and (max-width:480px) {
.fix-modal--alone{
top:0;
}
.drive-breadcrumb{
padding:12px !important;
}
.fix-form--inline{
.ant-form-item{flex:100%;}
}
}
================================================
FILE: packages/sharelist-web/src/assets/style/var.less
================================================
:root{
// --theme:100,52,248;
--theme:24,144,255;
--primary-color:rgb(var(--theme)); //rgb(105,65,199);
--primary-color-bg:rgba(var(--theme),.1); //rgb(105,65,199);
--primary-text-color:rgb(37, 38, 43);
--primary-background:rgb(var(--theme));
// --icon-folder-color:var(--primary-color);
// --icon-other-color:var(--primary-color);
// --icon-audio-color:rgb(132,140,239);
// --icon-video-color:rgb(219,68,55);
// --icon-word-color:rgb(47,151,254);
// --icon-pdf-color:rgb(252,134,132);
// --icon-ppt-color:rgb(254,159,93);
// --icon-file-color:rgb(212,214,218);
// --icon-word-color:rgb(88,178,252);
// --icon-doc-color:rgb(88,178,252);
// --icon-image-color:rgb(252,132,129);
--primary-text-secondary-color:rgba(37, 38, 43,0.36);
--color-main:var(--primary-text-color);
--primary-hover-bg-color:rgba(132, 133, 141, 0.08);
--primary-hover-theme-color:rgba(var(--theme), 0.08);
--dark-background:rgb(49, 49, 54);
--color-white-1:rgb(255,255,255);
--color-white-2:rgba(255,255,255,0.72);
--color-white-3:rgba(255,255,255,0.36);
--color-white-4:rgba(255,255,255,0.18);
--color-red:rgb(243, 91, 81);
--background:#ffffff;
--background-dark:rgb(17,17,19);
--background-header:#e3e6e9;
--background-header-dark:rgb(9,9,10);
--primary-text-color-dark:rgb(253,253,253);
--primary-progress:rgba(var(--theme),0.1);
--context:37, 38, 43;//0,0,0;
--context-1:rgb(var(--context));
--context-2:rgba(var(--context),0.72);
--context-3:rgba(var(--context),0.36);
--context-4:rgba(var(--context),0.18);
--context-background:#fff;
--context-background--dark:rgb(49,49,54);
--context-mask:#f8f9fa;
--context-hover:#f5f5f5;
--context-hover--dark:rgba(132, 133, 141,0.12);;
--primary-text-color--dark:rgb(37, 38, 43);
--divider: 132, 133, 141;
--divider-1: rgba(var(--divider), 0.2);
--divider-2: rgba(var(--divider), 0.16);
--divider-3: rgba(var(--divider), 0.08);
--danger-color:#ff4d4f;
}
@media (prefers-color-scheme: dark){
:root{
--background:var(--background-dark);
--background-header:var(--background-header-dark);
--primary-text-color:var(--primary-text-color-dark);
--context:255,255,255;
--context-background:var(--context-background--dark);
--context-hover:var(--context-hover--dark);
--primary-progress:rgba(var(--theme),0.2);
}
}
================================================
FILE: packages/sharelist-web/src/components/icon/icon-svg.js
================================================
!(function (t) {
var a,
l,
o,
i,
e,
c,
h =
'',
d = (d = document.getElementsByTagName('script'))[d.length - 1].getAttribute('data-injectcss')
if (d && !t.__iconfont__svg__cssinject__) {
t.__iconfont__svg__cssinject__ = !0
try {
document.write(
'',
)
} catch (t) {
console && console.log(t)
}
}
function n() {
e || ((e = !0), o())
}
; (a = function () {
var t, a, l
; ((l = document.createElement('div')).innerHTML = h),
(h = null),
(a = l.getElementsByTagName('svg')[0]) &&
(a.setAttribute('aria-hidden', 'true'),
(a.style.position = 'absolute'),
(a.style.width = 0),
(a.style.height = 0),
(a.style.overflow = 'hidden'),
(t = a),
(l = document.body).firstChild ? (a = l.firstChild).parentNode.insertBefore(t, a) : l.appendChild(t))
}),
document.addEventListener
? ~['complete', 'loaded', 'interactive'].indexOf(document.readyState)
? setTimeout(a, 0)
: ((l = function () {
document.removeEventListener('DOMContentLoaded', l, !1), a()
}),
document.addEventListener('DOMContentLoaded', l, !1))
: document.attachEvent &&
((o = a),
(i = t.document),
(e = !1),
(c = function () {
try {
i.documentElement.doScroll('left')
} catch (t) {
return void setTimeout(c, 50)
}
n()
})(),
(i.onreadystatechange = function () {
'complete' == i.readyState && ((i.onreadystatechange = null), n())
}))
})(window)
================================================
FILE: packages/sharelist-web/src/components/icon/index.less
================================================
// .sl-icon {
// display: inline-block;
// font-style: normal;
// vertical-align: -0.125em;
// text-align: center;
// text-transform: none;
// line-height: 0;
// text-rendering: optimizeLegibility;
// -webkit-font-smoothing: antialiased;
// }
/*
--icon-folder-color:var(--primary-color);
--icon-audio-color:rgb(132,140,239);
--icon-video-color:rgb(219,68,55);
--icon-word-color:rgb(47,151,254);
--icon-pdf-color:rgb(252,134,132);
--icon-ppt-color:rgb(254,159,93);
--icon-file-color:rgb(212,214,218);
--icon-word-color:rgb(88,178,252);
--icon-doc-color:rgb(88,178,252);
--icon-image-color:rgb(252,132,129);
*/
@type: {
icon-folder:#ffd55a;//#f8d673;
icon-file:rgb(188,190,194);//rgb(212,214,218);
icon-audio:rgb(223,94,83);
icon-video:rgb(223,94,83);
icon-image:rgb(223,94,83);
icon-word:rgb(96,181,252);
icon-doc:rgb(96,181,252);
icon-code:rgb(96,181,252);
icon-ppt:rgb(254,173,96);
icon-pdf:rgb(252,134,132);
}
// :root{
// each(@type, {
// --@{key}: @value;
// });
// }
each(@type, {
#@{key}{
color:~"var(--@{key}-color,@{value})";
}
});
================================================
FILE: packages/sharelist-web/src/components/icon/index.ts
================================================
import { createFromIconfontCN } from '@ant-design/icons-vue'
import config from '../../config/setting'
import './icon-svg'
import './index.less'
const IconFont = createFromIconfontCN({
scriptUrl: [],
})
export default IconFont
================================================
FILE: packages/sharelist-web/src/components/image/index.tsx
================================================
import { Image, Modal } from 'ant-design-vue'
export const showImage = (urls: Array, index: number) => {
const onVisibleChange = (e: any) => {
console.log(e)
if (e === false) {
modal.destroy()
}
}
const modal = Modal.confirm({
class: 'hide-modal',
width: '500px',
closable: true,
content: (
{
urls.map((url, idx) => )
}
),
})
}
================================================
FILE: packages/sharelist-web/src/components/player/index.less
================================================
.widget-player{
position: fixed;
bottom:0;
opacity: 0;
pointer-events: none;
// transform:translate(-50%,0);
transition:all 0.3s;
// left:50%;
max-width: 560px;
left:0;right:0;
margin:auto;
&.widget-player--visible{
opacity: 1;
pointer-events: auto;
bottom:16px;
}
.widget-player__tip{
opacity: .64;
font-size:10px;
}
.widget-player__action{
display: flex;
align-items: center;
flex:none;
}
.widget-player__progress{
position: absolute;
display: none;
width: 0%;
height:3px;
left:0;
bottom: 0;
background-color: var(--plyr-color-main);
transition:all 0.3s;
}
}
.widget-player__list{
// max-height: 0;
height: 0;
display: flex;
flex-direction: column;
transition:all .5s cubic-bezier(0.66, 0, 0.01, 1);
opacity: 0;
&.widget-player__list--visible{
height: 260px;
opacity: 1;
}
.widget-player__list-header{
color:#fff;
padding:8px;
text-align: center;
border-bottom:1px solid rgba(132,133,141,.2)
}
.widget-player__list-body{
overflow-y: auto;
overflow-x: hidden;
color:var(--player-color-main);
font-size: 13px;
margin-bottom: 0;
&::-webkit-scrollbar-track {
// box-shadow: inset 0 0 1px rgba(0,0,0,0.2);
background-color: rgba(0,0,0,0);
}
&::-webkit-scrollbar {
width: 5px;
// background-color: rgba(200,200,200,1);
}
&::-webkit-scrollbar-thumb {
background-color: rgba(200,200,200,.6);
}
}
.widget-player__list-item{
display: flex;
align-items: center;
padding:6px 16px;
cursor: pointer;
transition: all 0.3s;
&:hover{
background-color: var(--player-hover-background,rgba(0,0,0,0));
}
.widget-player__list-no{
flex:none;
padding-right: 8px;
text-align: right;
width:36px;
}
}
.widget-player__list-item--playing{
background-color: var(--player-fill,rgba(0,0,0,0)) !important;
}
}
.widget-player-wrap{
box-shadow: 0 1px 5px rgba(0,0,0,0.2);
background-color:var(--plyr-audio-background,#fff);
border-radius: 8px;
// overflow: hidden;
.widget-player__content{
display: none;
padding:0 12px;
color:var(--player-color-main);
}
.widget-player__body{
display: flex;
align-items: center;
justify-content: space-between;
flex:none;
position: relative;
z-index:1;
// background-color:var(--plyr-audio-background,#fff);
.widget-player__toggle-expand{
font-size:18px;
padding:0 12px;
cursor: pointer;
color:var(--plyr-audio-control-color,#000);
}
.widget-player__close{
flex:none;
font-size:18px;
padding:0 12px;
cursor: pointer;
color:var(--plyr-audio-control-color,#000);
}
.widget-player__btn-full{
flex:none;
font-size:18px;
padding:0 12px;
cursor: pointer;
color:var(--plyr-audio-control-color,#000);
display: none;
}
.widget-player__download{
flex:none;
font-size:18px;
padding:0 12px;
cursor: pointer;
color:var(--plyr-audio-control-color,#000);
}
}
}
.widget-player-audio{
.plyr{
min-width:300px;
}
}
.widget-player-video{
overflow: hidden;
.widget-player__progress{
display: block;
}
.widget-player__content{
display: block;
cursor: pointer;
min-width:200px;
.widget-player__content-title{
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
}
.widget-player__btn-full{
display: block !important;
}
&.widget-player--mini{
.plyr{
width:33.3%;
height:70px;
min-width:100px;
flex:none;
.plyr__controls{
display: none;
}
video{
object-fit: cover;
}
.plyr__video-wrapper{
margin: 0;
height: 100%;
}
}
}
}
.app-light{
--plyr-color-main:#00b3ff;
--plyr-audio-controls-background:rgba(0,0,0,0);
--plyr-audio-control-color:#ddd;
--plyr-control-radius:10px;
--plyr-audio-background:rgb(49,49,54);
--plyr-audio-control-background-hover:transparent;
--plyr-video-control-background-hover:transparent;
--plyr-video-background:rgba(0,0,0,0);
--player-color-main:#fff;
--player-hover-background:rgba(255,255,255,.1);
--player-fill:rgba(0,179,255,.5);
&::-webkit-scrollbar-track {
// box-shadow: inset 0 0 1px rgba(0,0,0,0.2);
background-color: #000;
}
&::-webkit-scrollbar {
width: 5px;
// background-color: rgba(200,200,200,1);
}
&::-webkit-scrollbar-thumb {
background-color: rgba(200,200,200,.6);
}
}
@media screen and (max-width:480px) {
.widget-player{
transform:translate(0,0);
left:0;width:100%;
bottom:-16px;
.widget-player-wrap{
border-radius: 0;
}
.widget-player-audio .plyr{
width:auto;
min-width:0px;
}
&.widget-player--visible{
bottom:0;
}
.widget-player__content{
min-width: 0;
}
}
}
================================================
FILE: packages/sharelist-web/src/components/player/index.tsx
================================================
import Plyr from 'plyr'
import { ref, reactive, defineComponent, onMounted, onUnmounted, computed, watch, watchEffect } from 'vue'
import 'plyr/dist/plyr.css'
import './index.less'
import { OrderedListOutlined, CloseOutlined, FullscreenOutlined, DownloadOutlined } from '@ant-design/icons-vue'
import { useBoolean, useState } from '@/hooks/useHooks'
const playerMap = new Map()
export const usePlayer = (id?: number | string): any => {
if (id && playerMap.has(id)) {
return playerMap.get(id)
}
const newId = id || playerMap.size + 1
const removePlayer = () => playerMap.delete(id)
const [state, setPlayer] = useState({
list: [],
type: '',
index: 0,
cur: { name: '', ctimeDisplay: '' },
})
const instance = {
id: newId,
data: state,
setPlayer,
removePlayer
}
playerMap.set(newId, instance)
return instance
}
export default defineComponent({
props: {
meidaId: {
type: Number,
required: true,
},
},
setup(props, ctx) {
const el = ref()
const { data, removePlayer } = usePlayer(props.meidaId)
const [visible, { setFalse: hidePlayer, setTrue: showPlayer }] = useBoolean()
const [visibleList, { toggle: toggleList }] = useBoolean()
const [fullscreen, { setFalse: existFullScreen, setTrue: enterFullScreen }] = useBoolean()
const playerProgress = ref('0%')
let player: any
const onClose = () => {
player.pause()
hidePlayer()
playerProgress.value = '0%'
}
const onSwitch = (idx: number) => {
const file: any = data.list[idx]
if (file) {
showPlayer()
data.index = idx
playerProgress.value = '0%'
player.source = {
type: data.type,
title: file.name,
sources: [{ src: file.preview_url || file.download_url, size: 'Raw' }],
}
data.cur = { ...file }
player.play()
}
}
const onFullScreen = () => {
enterFullScreen()
player.fullscreen.enter()
}
const onDownload = () => {
window.open(data.cur.download_url)
}
const onProgress = (e: any) => {
const plyr = e.detail.plyr
if (plyr.currentTime && plyr.duration) {
playerProgress.value = Math.floor((100 * plyr.currentTime) / plyr.duration) + '%'
}
}
const isIOS = /iphone|ipad|ipod/i.test(navigator.userAgent)
onMounted(() => {
player = new Plyr(el.value, {
fullscreen: { enabled: true, fallback: true, iosNative: isIOS, container: undefined }
})
player.on('exitfullscreen', existFullScreen)
player.on('timeupdate', onProgress)
})
onUnmounted(() => {
removePlayer()
})
watchEffect(() => {
onSwitch(data.index)
})
return () => (
)
},
})
================================================
FILE: packages/sharelist-web/src/config/api.ts
================================================
export type IAPI = [
name: string,
url: string | ((...args: any[]) => string),
options?: {
[key: string]: number | string | boolean
},
]
const api: IAPI[] = [
['userConfig', 'GET /api/user_config'],
['file', 'POST /api/drive/file/get', { token: true }],
['files', 'POST /api/drive/file/list', { token: true }],
['filePath', 'POST /api/drive/file/path', { token: true }],
// ['parents', 'GET /api/drive/files/:fileId/parents', { token: true }],
]
export default api
================================================
FILE: packages/sharelist-web/src/config/setting.ts
================================================
export default {}
================================================
FILE: packages/sharelist-web/src/hooks/useApi.ts
================================================
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { effectScope, EffectScope, App, InjectionKey, getCurrentInstance, inject } from 'vue'
export const apiSymbol = Symbol('api') as InjectionKey
// export type APIItemGroup = Array<[string, string | ((...args: Array) => string), Record]>
export type APIItem = [
name: string,
url: string | ((...args: Array) => string),
options?: {
[key: string]: number | string | boolean | ((...rest: Array) => any)
},
]
export type APICall = (...rest: Array) => Promise>
export interface IUseApi {
install?: (app: App) => void
_e?: EffectScope
_m?: any //Record
}
type RequestMethod = 'OPTIONS' | 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'TRACE' | 'CONNECT'
type ReqConfig = {
url: string
method: string
data?: any
params?: any
responseType?: string
token?: boolean
headers?: any
}
axios.defaults.timeout = 60 * 1000
const service: AxiosInstance = axios.create()
// http response 拦截器
service.interceptors.response.use(
(response) => {
return response.data
},
(error) => {
// 由接口返回的错误
if (error.response) {
return { error: { code: error.response.status, message: error.response.statusText } }
} else {
log(`服务器错误!错误代码:${error}`)
return { error: { code: error?.code || 500, message: '' } }
}
},
)
const log = (content: string, type = 'error'): void => {
console.log(content)
}
export type ReqResponse = {
error?: { code: number; message?: string; scope?: Record }
[key: string]: any
[key: number]: any
}
interface APIOptions {
inScope?: boolean
baseURL?: string
onReq?: (d: Record, itemOption: Record) => void
onRes?: (d: T) => void
onError?: (e: Error) => void
}
// const qs = (d: Record) => Object.keys(d).map(i => `${i}=${encodeURI(d[i])}`).join('&')
const urlReplace = (url: string, params: Record) =>
url.replace(/(?:\:)([\w\$]+)/g, ($0, $1) => {
if ($1 in params) {
return params[$1]
} else {
return ''
}
})
const convFormData = (data: any) => {
const fd = new FormData()
for (const i in data) {
if (Array.isArray(data[i])) {
const item = []
data[i].forEach((j: any, idx: number) => {
fd.append(`${i}[${idx}]`, j)
})
} else {
fd.append(i, data[i])
}
}
return fd
}
export const useApi = (options?: APIOptions) => {
if (globalApi) {
return globalApi._m as any
}
const currentInstance = getCurrentInstance()
const api = currentInstance && inject(apiSymbol)
if (!api) {
throw new Error(
'getActiveApi was called with no active api. Did you forget to install?\n' +
'\tconst api = createApi()\n' +
'\tapp.use(api)\n' +
`This will fail in production.`,
)
}
return api._m as any
}
const globalApi: IUseApi = {}
export const createApi = (apis: unknown, options?: APIOptions): IUseApi => {
type a = typeof apis
const pareKey: any = {}
for (const i of apis as Array) {
pareKey[i[0]] = 1
}
const apiMap: Record