注意:在保存之后,您可能需要清除浏览器缓存才能看到所作出的变更的影响。
?_=1
来访问最新页面。https://zh.moegirl.org.cn/User:%E6%9D%B1%E6%9D%B1%E5%90%9B/js/uploader.js?_=1
'use strict'
const cdnUrl = {
requireJs: 'https://unpkg.com/requirejs@2.3.6/require.js',
jss: 'https://unpkg.com/jss/dist/jss.min.js',
jssPreset: 'https://unpkg.com/jss-preset-default/dist/jss-preset-default.min.js',
vue: 'https://unpkg.com/vue@2.6.11/dist/vue.min.js'
// vue: 'https://unpkg.com/vue@2.6.11/dist/vue.js'
}
/**
* @param {string} sourceUrl
*/
function loadScript(sourceUrl) {
return new Promise((resolve, reject) => {
const scriptTag = document.createElement('script')
scriptTag.src = sourceUrl
document.body.appendChild(scriptTag)
scriptTag.addEventListener('load', resolve)
scriptTag.addEventListener('error', reject)
})
}
/**
* @param {Array<object>} modulePaths
* type ModulePaths = { [moduleName: string]: string }[]
*/
async function loadModules(modulePaths) {
return new Promise((resolve, reject) => {
const trimTailJs = url => url.replace(/\.js$/, '')
const paths = modulePaths.reduce((result, item) => {
result[Object.keys(item)[0]] = trimTailJs(Object.values(item)[0])
return result
}, {})
const loadModuleNames = modulePaths.map(item => Object.keys(item)[0])
require.config({ paths })
require(loadModuleNames, (...modules) => resolve(modules), reject)
})
}
/**
* @param {object} data
*/
function request(data) {
data.origin = 'https://zh.moegirl.org.cn'
return new Promise(function (resolve, reject) {
$.ajax({
url: 'https://commons.moegirl.org.cn/api.php',
type: 'post',
timeout: 15000,
xhrFields: { withCredentials: true },
data: data
}).done(resolve).fail(reject)
})
}
/**
* @param {string} word
*/
function getHints(word) {
return request({
"action": "query",
"format": "json",
"list": "search",
"srsearch": word,
"srnamespace": "14",
"srlimit": "20"
})
}
function getToken() {
return request({
"action": "query",
"format": "json",
"meta": "tokens",
}).then(function (data) {
return data.query.tokens.csrftoken
})
}
/**
* @param {object} options
* @param {File} options.body
* @param {string} options.fileName
* @param {string} options.comment
* @param {string} options.pageContent
*/
function upload({ body, fileName, comment, pageContent }) {
return new Promise(function (resolve, reject) {
getToken()
.then(function (token) {
var data = {
filename: fileName,
comment: comment,
text: pageContent,
action: 'upload',
format: 'json',
ignorewarnings: true,
token: token,
origin: "https://zh.moegirl.org.cn"
}
if (typeof body == 'string') {
data.url = body
} else {
data.file = body
}
var formData = new FormData()
Object.keys(data).forEach(function (key) {
formData.append(key, data[key])
})
$.ajax({
url: 'https://commons.moegirl.org.cn/api.php',
type: 'post',
timeout: 20000,
xhrFields: { withCredentials: true },
contentType: false,
processData: false,
data: formData
})
.done(data => {
if ('error' in data) return reject(data.error)
resolve(data)
})
.fail(() => reject())
})
.catch(reject)
})
}
/**
* @param {string[]} fileNames
*/
function checkFileNames(fileNames) {
return request({
action: 'query',
format: 'json',
titles: fileNames.map(item => 'File:' + item).join('|'),
prop: '',
}).then(function (data) {
return Object.values(data.query.pages).reduce((result, item) => {
result[item.title.replace('File:', '')] = !('missing' in item)
return result
}, {})
})
}
$(() => (async () => {
//await loadScript(cdnUrl.requireJs)
//const [
// { default: jss },
// { default: jssPreset },
// Vue,
//] = await loadModules([
// { jss: cdnUrl.jss },
// { jssPreset: cdnUrl.jssPreset },
// { Vue: cdnUrl.vue }
//])
//jss.setup(jssPreset())
//$(document.body).append('<div id="widget-fileUploader" style="display:none">')
// 向“更多”菜单注入按钮
$('#p-cactions ul').append('<li id="btn-fileUploader"><a title="上传文件">上传文件</a></li>')
$('#btn-fileUploader').click(() => {
alert('因技术问题,请前往图站使用批量上传工具')
window.open('https://commons.moegirl.org.cn/MediaWiki:Uploader', '_blank')
// $('#widget-fileUploader').fadeIn(200)
// $('#content').css('position', 'static')
})
return false
const template = `
<div id="widget-fileUploader" :class="s.container">
<input
ref="fileInput"
style="display:none"
type="file"
multiple="multiple"
:accept="allowedFileTypes.map(item => '.' + item).join(',')"
@change="addFileByFileSelector"
/>
<div :class="s.closeBtn" @click="hideWidget">×</div>
<div :class="s.body">
<div
:class="s.fileList"
@dragenter.prevent="() => {}"
@dragover.prevent="() => {}"
@drop.prevent="addFileByDropping"
>
<div
v-if="files.length === 0"
key="hintMask"
class="hintMask"
@click="$refs.fileInput.click()"
>
<div class="hintText">点此添加文件,或将文件拖放至此</div>
</div>
<div
v-for="(item, index) in files"
:key="item.body.lastModified"
class="item"
:data-name="item.fileName"
:data-selected="index === focusedFileIndex"
title="单击选中文件,双击复制文件名"
@click="focusFile(index)"
>
<img
v-if="isImageFile(item.body)"
:src="item.objectUrl"
/>
<div v-else class="unablePreviewHint">
<div>不支持预览的文件类型</div>
<div
v-if="typeof item.body !== 'string'"
class="type"
>Mimetype: {{ item.body.type }}</div>
</div>
<div class="removeBtn" @click.stop="files.splice(index, 1)">×</div>
</div>
<div
v-if="files.length !== 0"
class="item addFileBox"
@click="$refs.fileInput.click()"
/>
</div>
<div :class="s.panel">
<div class="block">
<div class="input-container" title="上传后使用文件时的名字,要求不能和现有文件重复">
<span>文件名:</span>
<input v-model.trim="form.fileName" />
</div>
<div class="input-container categoryInput" title="所有文件共享分类">
<span>分 类:</span>
<input
ref="categoryInput"
v-model.trim="form.categoryInput"
@input="loadCategoryHint"
@keydown.enter="addCategory(form.categoryInput)"
@keydown.up.prevent="handlerFor_categoryInput_wasKeyDowned"
@focus="categoryFocused = true"
@blur="categoryFocused = false"
/>
<div class="inputHint">按下回车添加分类</div>
<div
ref="categoryHints"
v-if="categoryFocused && categoryHints.length !== 0"
class="categoryHints"
tabindex="0"
@keydown.enter="addCategory(categoryHints[categoryHintFocusedIndex])"
@keydown.prevent="handlerFor_categoryHints_wasKeyDowned"
@focus="categoryFocused = true"
@blur="categoryFocused = false"
>
<div
v-for="(item, index) in categoryHints"
class="item"
:data-selected="index === categoryHintFocusedIndex"
@click="addCategory(item)"
>{{ item }}</div>
</div>
</div>
<div class="categories">
<div
v-for="(item, index) in form.categories"
class="item"
title="点击删除分类"
@click="form.categories.splice(index, 1)"
>{{ item }}</div>
</div>
</div>
<div class="block">
<div class="input-container">
<span>角色名:</span>
<input v-model.trim="form.charaName" />
</div>
<div class="input-container">
<span>作 者:</span>
<input v-model.trim="form.author" />
</div>
<div class="input-container">
<span>源地址:</span>
<input v-model.trim="form.source" />
</div>
</div>
<div
class="block"
style="flex-direction:column; justify-content:space-around; align-items:flex-start;"
>
<div class="input-container" title="所有文件共享前缀">
<span>添加前缀:</span>
<input v-model.trim="form.prefix" style="width:calc(100% - 6em)" />
</div>
<div
class="input-container"
style="justify-content:flex-start;"
>
<select v-model.trim="form.license">
<option disabled="disabled" value="">选择授权协议(将鼠标放在选项上显示详情)</option>
<optgroup label="CC协议">
<option value="CC Zero" title="作者授权以无著作权方式使用">CC-0</option>
<option value="CC BY" title="作者授权以署名方式使用,该授权需兼容3.0协议">CC BY 3.0</option>
<option value="CC BY-SA" title="作者授权以署名-相同方式方式使用,该授权需兼容3.0协议">CC BY-SA 3.0</option>
<option value="CC BY-NC-SA" title="作者授权以署名-非商业使用-相同协议方式使用,该授权需兼容3.0协议">CC BY-NC-SA 3.0</option>
</optgroup>
<optgroup label="公有领域">
<option value="PD-Old">作者离世一定年限后流入公有领域</option>
<option value="PD-Other">其他原因流入公有领域</option>
</optgroup>
<optgroup label="其他">
<option value="Copyright" title="原作者没有明确的授权声明">原作者保留权利</option>
<option value="none:gotoCommons">原作者授权萌百使用</option>
<option value="可自由使用" title="作者放弃版权或声明可自由使用">可自由使用</option>
<option value="萌娘百科版权所有">萌娘百科版权所有</option>
</optgroup>
</select>
</div>
<div class="buttons">
<button @click="addSourceUrlFile">添加源地址文件</button>
<button :disabled="status === 2" title="执行上传文件" @click="submit(false)">上传</button>
<button
:disabled="status === 2"
title="在发生文件名已存在的情况时,自动滤掉已存在的文件。通常用于在上一次批量上传中一部分失败后,再次尝试将之前没传上去的文件重新上传"
@click="submit(true)"
>差分上传</button>
<button title="将当前文件除文件名的信息同步到全部文件" @click="asyncCurrentFileInfo">同步文件信息</button>
<button @click="showManual">使用说明</button>
</div>
</div>
</div>
</div>
</div>
`
new Vue({
el: '#widget-fileUploader',
template,
data() {
return {
s: createStyles(), // 样式
allowedFileTypes: ['ogg', 'ogv', 'oga', 'flac', 'opus', 'wav', 'webm', 'mp3', 'png', 'gif', 'jpg', 'jpeg', 'webp', 'svg', 'pdf', 'ppt', 'jp2', 'doc', 'docx', 'xls', 'xlsx', 'psd', 'sai', 'swf', 'mp4'],
files: [], // 待上传的文件
focusedFileIndex: 0,
categoryHints: [],
categoryInputDebounceTimeoutKey: 0,
categoryHintFocusedIndex: -1,
categoryFocused: false,
status: 1, // 0:失败,1:初始化,2:提交中,3:成功
form: {
fileName: '',
categoryInput: '', // 分类输入栏
categories: [], // 实际要提交的分类
charaName: '',
author: '',
source: '',
prefix: '',
license: ''
},
doubleClickTimeoutKey: 0 // 用于双击复制文件名
}
},
mounted() {
$('#widget-fileUploader').hide()
},
watch: {
files() {
this.focusedFileIndex === 0 && this.focusFile(0)
},
form: {
deep: true,
handler() {
if (!this.files[this.focusedFileIndex]) { return }
this.files[this.focusedFileIndex] = {
...this.files[this.focusedFileIndex],
fileName: this.form.fileName,
author: this.form.author,
charaName: this.form.charaName,
source: this.form.source,
license: this.form.license
}
}
},
license(val) {
if (val === 'none:gotoCommons') {
alert('该协议需要手动填写授权证明,请到共享站进行上传')
window.open('https://commons.moegirl.org.cn/Special:上传文件', '_blank')
}
}
},
computed: {
license() {
return this.form.license
},
},
methods: {
createFileItem(fileBody) {
return {
body: fileBody,
objectUrl: typeof fileBody === 'string' ? fileBody : URL.createObjectURL(fileBody),
fileName: typeof fileBody === 'string' ? fileBody.replace(/.+\/(.+?)$/, '$1') : fileBody.name,
author: '',
charaName: '',
source: '',
license: 'Copyright'
}
},
isImageFile(fileBody) {
const imageType = ['jpg', 'png', 'jpeg', 'gif', 'webp']
return imageType.includes((typeof fileBody === 'string' ? fileBody : fileBody.name).replace(/.+\.(.+?)$/, '$1'))
},
hideWidget() {
$('#widget-fileUploader').fadeOut(200)
$('#content').css('position', 'relative')
},
loadCategoryHint() {
clearTimeout(this.categoryInputDebounceTimeoutKey)
this.categoryInputDebounceTimeoutKey = setTimeout(() => {
if (this.form.categoryInput === '') { return }
getHints(this.form.categoryInput)
.then(data => {
const hints = data.query.search.map(item => item.title.split('Category:')[1])
this.categoryHints = hints
})
}, 500)
},
resetCategory() {
this.form.categoryInput = ''
this.categoryHints = []
this.categoryHintFocusedIndex = -1
},
addCategory(categoryName) {
if (this.form.categories.includes(categoryName)) return mw.notify('请不要重复添加分类')
this.form.categories.push(categoryName)
this.resetCategory()
},
// 实现上下键切换分类提示
handlerFor_categoryHints_wasKeyDowned(e) {
if (e.code === 'ArrowUp') {
this.categoryHintFocusedIndex++
if (this.categoryHintFocusedIndex > this.categoryHints.length - 1) {
this.categoryHintFocusedIndex = 0
}
}
if (e.code === 'ArrowDown') {
this.categoryHintFocusedIndex--
if (this.categoryHintFocusedIndex < 0) {
this.$refs.categoryInput.focus()
}
}
this.categoryHintFocusedIndex >= 0 && this.$refs.categoryHints.querySelectorAll('div')[this.categoryHintFocusedIndex].scrollIntoView()
},
handlerFor_categoryInput_wasKeyDowned() {
if (this.categoryHints.length === 0 || !this.$refs.categoryHints) { return }
this.$refs.categoryHints.focus()
this.categoryHintFocusedIndex = 0
},
addFileByFileSelector(e) {
const originalFileList = e.target.files
;[].forEach.call(originalFileList, file => {
if (this.files.length === 50) { return }
if (file.size / 1024 / 1024 > 20) return alert(`文件【${file.name}】大小超过20m,无法上传!`)
this.files.push(this.createFileItem(file))
})
e.target.value = ''
if (this.files.length === 50) mw.notify('一次最多上传50个文件', { type: 'wran' })
},
addFileByDropping(e) {
const originalFileList = e.dataTransfer.files
;[].forEach.call(originalFileList, file => {
if (this.files.length === 50) { return }
if (!this.allowedFileTypes.includes(file.name.replace(/.+\.(.+?)$/, '$1'))) return alert(`【${file.name}】不支持上传这种格式的文件!`)
if (file.size / 1024 / 1024 > 20) return alert(`【${file.name}】的大小超过20m,无法上传!`)
this.files.push(this.createFileItem(file))
})
if (this.files.length === 50) mw.notify('一次最多上传50个文件', { type: 'wran' })
},
focusFile(index) {
this.focusedFileIndex = index
const file = this.files[index]
this.form = {
...this.form,
fileName: file.fileName,
author: file.author,
charaName: file.charaName,
source: file.source,
license: file.license
}
// 实现双击复制文件名
if (this.doubleClickTimeoutKey === 0) {
this.doubleClickTimeoutKey = setTimeout(() => {
this.doubleClickTimeoutKey = 0
}, 300)
} else {
mw.notify('已复制文件名')
this.copyFileName(this.form.prefix + file.fileName)
clearTimeout(this.doubleClickTimeoutKey)
this.doubleClickTimeoutKey = 0
}
},
addSourceUrlFile() {
var url = (prompt('请输入文件地址:') || '').trim()
if (!url) { return }
this.files.push(this.createFileItem(url))
},
copyFileName(fileName) {
const inputTag = document.createElement('input')
inputTag.value = fileName
inputTag.style.cssText = `
position: fixed;
left: -9999px;
`
document.body.appendChild(inputTag)
inputTag.focus()
document.execCommand('selectAll')
document.execCommand('copy')
setTimeout(() => document.body.removeChild(inputTag), 1000)
},
asyncCurrentFileInfo() {
if (!confirm('确定要将当前选中的文件信息(不含文件名)同步到所有文件中?')) { return }
const currentFile = this.files[this.focusedFileIndex]
if (!currentFile) return mw.notify('当前未选中文件')
this.files.forEach(item => {
item.author = currentFile.author
item.charaName = currentFile.charaName
item.source = currentFile.source
item.license = currentFile.license
})
mw.notify('已同步')
},
showManual() {
alert([
'使用说明',
'1. 该插件是一个文件上传工具,免去前往萌娘共享站再上传文件的麻烦。同时支持拖拽上传,批量上传。',
'2. 若文件上传时发生异常,请以萌娘共享站的监视列表为准。',
'3. 每个文件拥有独立的信息,但“分类”和“添加前缀”是共享的。在需要同步每个文件的角色名、作者等信息时可以使用“同步文件信息”的功能。',
'4. 什么是“差分上传”:在发生文件名已存在的情况时,自动滤掉已存在的文件。通常用于在上一次批量上传中一部分失败后,再次尝试将之前没传上去的文件重新上传。',
'5. 双击文件可以自动复制“前缀 + 文件名”。'
].join('\n'))
},
async submit(diffMode) {
if (this.files.length === 0) return mw.notify('您还没有上传任何文件', { type: 'warn' })
if (this.files.some(item => item.fileName === '')) return mw.notify('存在文件名为空的文件', { type: 'warn' })
const duplicateFiles = this.files.reduce((result, item) => {
const isDuplicate = this.files.filter(item2 => item2.fileName === item.fileName).length > 1
isDuplicate && result.push(item)
return result
}, [])
if (duplicateFiles.length > 0) return alert([
'这些文件名发生了重复,请不要给要上传的文件设置相同的名称:',
...duplicateFiles.map(item => item.fileName)
].join('\n'))
const authorizedForMoegirlFiles = this.files.filter(item => item.license === 'none:gotoCommons')
if (authorizedForMoegirlFiles.length > 0) return alert([
'这些文件的授权协议不允许使用上传工具,请在本次上传中删除,并前往共享站填写授权信息后上传:',
...authorizedForMoegirlFiles.map(item => item.fileName),
].join('\n'))
if (!confirm('确定要开始上传吗?')) { return }
let postData = this.files.map(item => {
const metaCategories =
(item.charaName ? `[[分类:${item.charaName}]]` : '') +
(item.author ? `[[分类:作者:${item.author}]]` : '')
const source = item.source ? `源地址:${item.source}` : ''
const comment = metaCategories + source
const pageContent = [
'== 文件说明 ==',
metaCategories + this.form.categories.map(item => `[[分类:${item}]]`).join(''),
source,
'== 授权协议 ==',
`{{${item.license}}}`
].join('\n')
return {
body: item.body,
fileName: this.form.prefix + item.fileName,
comment,
pageContent
}
})
mw.notify(`开始${diffMode ? '差分' : ''}上传,共${postData.length}个文件...`)
console.log(`---- Moegirl:fileUploader 开始${diffMode ? '差分' : ''}上传,共${postData.length}个文件 ----`)
this.status = 2
const printLogFn = (type = 'info') => msg => { mw.notify(msg, { type }); console.log(msg) }
const printLog = printLogFn()
printLog.warn = printLogFn('warn')
printLog.error = printLogFn('error')
try {
const checkedResult = await checkFileNames(postData.map(item => item.fileName))
const existedFiles = postData.filter(item => checkedResult[item.fileName.replace(/^./, s => s.toUpperCase())]) // 首字母转大写,因为checkedResult返回的文件名首字母是大写
if (existedFiles.length > 0 && !diffMode) {
alert([
'这些文件名已被使用,请为对应的文件更换其他名称:',
...existedFiles.map(item => item.fileName)
].join('\n'))
this.status = 1
return
}
if (diffMode) postData = postData.filter(item => !checkedResult[item.fileName.replace(/^./, s => s.toUpperCase())])
if (diffMode && postData.length === 0) {
alert('差分模式下没有可以上传的文件')
this.status = 1
return
}
printLog.warn(`${diffMode ? '差分上传' : ''}共需要上传${postData.length}个文件`)
let uploadResults = []
if (postData.length <= 3) {
uploadResults = await Promise.all(
postData.map(item => new Promise(resolve => {
upload(item)
.then(() => {
printLog(`【${item.fileName}】上传成功`)
resolve({ fileName: item.fileName, result: true })
})
.catch(error => {
printLog.error(`【${item.fileName}】上传失败`)
resolve({
fileName: item.fileName,
result: false,
...(error ? { errorInfo: error.info } : {})
})
})
}))
)
} else {
alert('上传的文件超过个三个,执行分段上传,请耐心等待。进入控制台可查看全部日志(按F12后选择Console)。')
printLog.warn('上传文件超过3个,执行分段上传')
// 分段上传
const segmentedPostData = postData.reduce((result, item) => {
if (result.length === 0) result.push([])
if (result[result.length - 1].length === 3) result.push([])
result[result.length - 1].push(item)
return result
}, [])
console.log(segmentedPostData)
for (let i=0, len=segmentedPostData.length; i < len; i++) {
printLog(`共${len}个分段,现在开始第${i + 1}个`)
const segment = segmentedPostData[i]
const segmentedUploadResult = await Promise.all(
segment.map(item => new Promise(resolve => {
upload(item)
.then(() => {
printLog(`【${item.fileName}】上传成功`)
resolve({ fileName: item.fileName, result: true })
})
.catch(error => {
printLog.error(`【${item.fileName}】上传失败`)
resolve({
fileName: item.fileName,
result: false,
...(error ? { errorInfo: error.info } : {})
})
})
}))
)
uploadResults.push(...segmentedUploadResult)
printLog(`第${i + 1}个分段完成,其中${segmentedUploadResult.filter(item => item.result).length}个成功,${segmentedUploadResult.filter(item => !item.result).length}个失败`)
}
}
const report = [
`全部上传结果:共计${uploadResults.length}个文件,其中${uploadResults.filter(item => item.result).length}个成功,${uploadResults.filter(item => !item.result).length}个失败`,
...uploadResults.map((item, index) => `${index + 1}. 【${item.fileName}】${item.result ? '成功' : '失败'}`)
].join('\n')
console.log(report)
alert(report)
const errorInfoList = uploadResults.filter(item => item.errorInfo)
if (errorInfoList.length > 0) {
const errorInfo = '这些文件返回了错误信息:\n' + errorInfoList
.map(item => item.fileName + ':' + item.errorInfo)
.join('\n')
console.log(errorInfo)
alert(errorInfo)
}
this.status = 3
} catch (e) {
console.log('上传流程出现错误', e)
mw.notify('网络错误,请重试', { type: 'error' })
this.status = 0
}
}
}
})
function createStyles() {
return jss.createStyleSheet({
container: {
width: '100%',
height: '100%',
position: 'fixed',
top: 0,
left: 0,
backgroundColor: 'rgba(0, 0, 0, 0.3)',
zIndex: 100
},
closeBtn: {
fontSize: 30,
fontWeight: 'bold',
color: 'white',
fontFamily: 'Simsun',
position: 'fixed',
top: 10,
right: 20,
transition: 'transform 0.3s',
zIndex: 10001,
cursor: 'pointer',
'&:hover': {
transform: 'rotate(90deg)'
}
},
body: {
minWidth: 800,
maxWidth: 930,
height: 500,
backgroundColor: 'white',
borderRadius: 10,
border: '5px #eee solid',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
margin: 'auto'
},
fileList: {
height: '70%',
backgroundColor: 'white',
borderRadius: '10px 10px 0 0',
position: 'relative',
borderBottom: '3px #ccc solid',
boxSizing: 'border-box',
overflow: 'auto',
cursor: 'pointer',
paddingBottom: 10,
'& .hintMask': {
position: 'absolute',
width: '100%',
height: '100%',
top: 0,
left: 0,
'&::before, &::after': {
content: '""',
width: 40,
height: 150,
backgroundColor: '#ddd',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
margin: 'auto'
},
'&::after': {
width: 150,
height: 40
},
'& > .hintText': {
fontSize: 22,
color: '#ddd',
position: 'absolute',
left: '50%',
transform: 'translateX(-50%)',
bottom: 30,
whiteSpace: 'nowrap'
}
},
'& > .item': {
width: 200,
height: 150,
boxSizing: 'border-box',
backgroundColor: 'white',
marginLeft: 10,
marginTop: 10,
border: '1px #ccc solid',
display: 'inline-block',
position: 'relative',
cursor: 'pointer',
verticalAlign: 'middle',
'&.addFileBox': {
'&::before, &::after': {
content: '""',
width: 15,
height: 60,
backgroundColor: '#ddd',
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
right: 0,
margin: 'auto'
},
'&::after': {
width: 60,
height: 15
}
},
'&[data-selected="true"]': {
borderColor: '#4EBE8C',
'&::after': {
content: '""',
display: 'block',
position: 'absolute',
width: '100%',
height: '100%',
top: 0,
left: 0,
boxSizing: 'border-box',
border: '3px #4EBE8C solid',
pointerEvents: 'none'
}
},
'&::before': {
content: 'attr(data-name)',
display: 'block',
width: '100%',
position: 'absolute',
bottom: 0,
left: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
color: 'white',
fontSize: 13,
textAlign: 'center',
lineHeight: '25px',
overflow: 'hidden',
height: 25,
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
boxSizing: 'border-box',
padding: '0 10px'
},
'& > img': {
width: '100%',
height: '100%',
padding: 5,
boxSizing: 'border-box',
objectFit: 'scale-down'
},
'& > .unablePreviewHint': {
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
color: '#666',
fontSize: 13,
'& > .type': {
width: '80%',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}
},
'& > .removeBtn': {
width: 20,
height: 20,
borderRadius: '50%',
textAlign: 'center',
lineHeight: '20px',
fontWeight: 'bold',
fontFamily: '黑体',
position: 'absolute',
top: 5,
right: 5,
'&:hover': {
backgroundColor: '#666',
color: 'white'
}
}
}
},
panel: {
height: '30%',
padding: 10,
boxSizing: 'border-box',
display: 'flex',
'& > .block': {
display: 'flex',
flex: 1,
flexWrap: 'wrap',
alignItems: 'center',
height: '100%',
padding: '0 10px',
'& .input-container': {
minWidth: 240,
position: 'relative',
'& > *': {
verticalAlign: 'middle',
fontSize: 14,
},
'& > input': {
boxSizing: 'border-box',
width: 'calc(100% - 5em)',
minWidth: 150
}
}
},
'& .categoryInput': {
position: 'relative',
'& .inputHint': {
opacity: 0,
transition: 'opacity 0.2s',
backgroundColor: '#fffeee',
border: '1px #ccc solid',
padding: '2px 10px',
position: 'absolute',
bottom: 'calc(100% - 7px)',
left: 'calc(100% - 7px)',
zIndex: 1,
borderRadius: 5,
whiteSpace: 'nowrap'
},
'& > input:focus + .inputHint': {
opacity: 1
}
},
'& .categoryHints': {
minWidth: 170,
maxHeight: 140,
backgroundColor: 'white',
whiteSpace: 'nowrap',
overflow: 'auto',
position: 'absolute',
right: 9,
bottom: '100%',
border: '1px #666 solid',
boxSizing: 'border-box',
borderBottom: 'none',
display: 'flex',
flexDirection: 'column-reverse',
'& > .item': {
minHeight: 20,
lineHeight: '20px',
boxSizing: 'border-box',
padding: '0 5px',
width: '100%',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
cursor: 'pointer',
'&[data-selected="true"]': {
backgroundColor: '#ccc'
}
}
},
'& .categories': {
width: '100%',
height: 23,
border: '1px #ccc solid',
borderRadius: 5,
overflow: 'auto',
marginRight: 5,
boxSizing: 'border-box',
'& > .item': {
display: 'inline-block',
lineHeight: '15px',
textAlign: 'center',
border: '1px #666 solid',
backgroundColor: '#eee',
margin: '2px 3px',
padding: '0 5px',
fontSize: 14,
cursor: 'pointer'
}
},
'& .buttons': {
width: '100%',
'& > button': {
marginTop: 5
}
}
}
}).attach().classes
}
})())