diff --git a/package.json b/package.json
index 41e56226..90fffc52 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,6 @@
"@fortawesome/fontawesome-free": "^5.2.0",
"admin-lte": "^2.4.2",
"bootstrap": "^3.3.7",
- "bootstrap-fileinput": "^4.4.7",
"chart.js": "^2.7.1",
"es6-promise": "^4.2.4",
"highlight.js": "^9.12.0",
@@ -36,6 +35,7 @@
"toastr": "^2.1.4",
"vue": "^2.5.16",
"vue-good-table": "^2.12.2",
+ "vue-upload-component": "^2.8.11",
"vuejs-paginate": "^2.0.1",
"whatwg-fetch": "^2.0.4"
},
diff --git a/resources/assets/src/components/route.js b/resources/assets/src/components/route.js
index 99967821..8054a370 100644
--- a/resources/assets/src/components/route.js
+++ b/resources/assets/src/components/route.js
@@ -59,4 +59,9 @@ export default [
component: () => import('./skinlib/List'),
el: '.content-wrapper'
},
+ {
+ path: 'skinlib/upload',
+ component: () => import('./skinlib/Upload'),
+ el: '.content'
+ },
];
diff --git a/resources/assets/src/components/skinlib/Upload.vue b/resources/assets/src/components/skinlib/Upload.vue
new file mode 100644
index 00000000..55c7bde5
--- /dev/null
+++ b/resources/assets/src/components/skinlib/Upload.vue
@@ -0,0 +1,222 @@
+
+
+
+
+
+
+
diff --git a/resources/assets/src/js/layout.js b/resources/assets/src/js/layout.js
index 5f65ff2d..c4b8c8ea 100644
--- a/resources/assets/src/js/layout.js
+++ b/resources/assets/src/js/layout.js
@@ -1,6 +1,5 @@
import './jquery'; // jQuery first
import 'bootstrap';
-import 'bootstrap-fileinput';
import 'admin-lte';
import 'icheck';
import Vue from 'vue';
diff --git a/resources/assets/src/js/net.js b/resources/assets/src/js/net.js
index 69e169a9..409aa713 100644
--- a/resources/assets/src/js/net.js
+++ b/resources/assets/src/js/net.js
@@ -10,7 +10,7 @@ const init = {
}
};
-async function walkFetch(request) {
+export async function walkFetch(request) {
try {
const response = await fetch(request);
if (response.ok) {
diff --git a/resources/assets/tests/components/skinlib/Upload.test.js b/resources/assets/tests/components/skinlib/Upload.test.js
new file mode 100644
index 00000000..8564861b
--- /dev/null
+++ b/resources/assets/tests/components/skinlib/Upload.test.js
@@ -0,0 +1,138 @@
+import Vue from 'vue';
+import { mount } from '@vue/test-utils';
+import Upload from '@/components/skinlib/Upload';
+import { flushPromises } from '../../utils';
+import toastr from 'toastr';
+import { swal } from '@/js/notify';
+import { walkFetch } from '@/js/net';
+
+jest.mock('toastr');
+jest.mock('@/js/notify');
+jest.mock('@/js/net');
+
+window.__bs_data__ = {
+ textureNameRule: 'rule',
+ privacyNotice: 'privacyNotice',
+ scorePrivate: 10,
+ scorePublic: 1
+};
+
+test('display drap and drop notice', () => {
+ const wrapper = mount(Upload, {
+ stubs: ['file-upload']
+ });
+ expect(wrapper.text()).toContain('skinlib.upload.dropZone');
+ wrapper.setData({ files: [{}] });
+ expect(wrapper.contains('img')).toBeTrue();
+});
+
+test('button for removing texture', () => {
+ const wrapper = mount(Upload, {
+ stubs: ['file-upload']
+ });
+ wrapper.vm.$refs = {
+ upload: {
+ clear: jest.fn()
+ }
+ };
+ const button = wrapper.find('.btn-default');
+ expect(button.isVisible()).toBeFalse();
+ wrapper.setData({ files: [{}] });
+ expect(button.isVisible()).toBeTrue();
+ button.trigger('click');
+ expect(wrapper.vm.texture).toBe('');
+});
+
+test('notice should be display if texture is private', () => {
+ const wrapper = mount(Upload, {
+ stubs: ['file-upload']
+ });
+ expect(wrapper.contains('.callout')).toBeFalse();
+ wrapper.find('[type=checkbox]').setChecked();
+ expect(wrapper.find('.callout').text()).toBe('privacyNotice');
+});
+
+test('display score cost', () => {
+ const origin = Vue.prototype.$t;
+ Vue.prototype.$t = (key, args) => `${key}${JSON.stringify(args)}`;
+
+ const wrapper = mount(Upload, {
+ stubs: ['file-upload']
+ });
+ wrapper.find('[type=checkbox]').setChecked();
+ wrapper.setData({ files: [{ size: 1024 }] });
+ expect(wrapper.text()).toContain(JSON.stringify({ score: 10 }));
+
+ Vue.prototype.$t = origin;
+});
+
+test('process input file', () => {
+ window.URL.createObjectURL = jest.fn().mockReturnValue('file-url');
+ const blob = new Blob;
+ const wrapper = mount(Upload, {
+ stubs: ['file-upload']
+ });
+
+ wrapper.vm.inputFile();
+ expect(wrapper.vm.name).toBe('');
+
+ wrapper.vm.inputFile({ file: blob, name: '123.png' });
+ expect(wrapper.vm.name).toBe('123');
+
+ wrapper.vm.inputFile({ file: blob, name: '456.png' });
+ expect(wrapper.vm.name).toBe('123');
+
+ wrapper.setData({ name: '' });
+ wrapper.vm.inputFile({ file: blob, name: '789' });
+ expect(wrapper.vm.name).toBe('789');
+
+ expect(wrapper.vm.texture).toBe('file-url');
+});
+
+test('upload file', async () => {
+ window.Request = jest.fn();
+ walkFetch
+ .mockResolvedValueOnce({ errno: 1, msg: '1' })
+ .mockResolvedValueOnce({ errno: 0, msg: '0', tid: 1 });
+ jest.spyOn(toastr, 'info');
+ swal.mockReturnValue();
+
+ const wrapper = mount(Upload, {
+ stubs: ['file-upload']
+ });
+ const button = wrapper.find('.box-footer > button');
+
+ button.trigger('click');
+ expect(walkFetch).not.toBeCalled();
+ expect(toastr.info).toBeCalledWith('skinlib.emptyUploadFile');
+
+ wrapper.setData({ files: [{ file: {}, name: 't', type: 'image/jpeg' }] });
+ button.trigger('click');
+ expect(walkFetch).not.toBeCalled();
+ expect(toastr.info).toBeCalledWith('skinlib.emptyTextureName');
+
+ wrapper.find('[type=text]').setValue('t');
+ button.trigger('click');
+ expect(walkFetch).not.toBeCalled();
+ expect(toastr.info).toBeCalledWith('skinlib.fileExtError');
+
+ wrapper.setData({ files: [{ file: new Blob, name: 't.png', type: 'image/png' }] });
+ button.trigger('click');
+ await flushPromises();
+ expect(window.Request).toBeCalledWith('/skinlib/upload', {
+ body: expect.any(FormData),
+ credentials: 'same-origin',
+ headers: {
+ Accept: 'application/json'
+ },
+ method: 'POST'
+ });
+ expect(walkFetch).toBeCalledWith(expect.any(window.Request));
+ expect(swal).toBeCalledWith({ type: 'warning', text: '1' });
+
+ button.trigger('click');
+ await flushPromises();
+ jest.runAllTimers();
+ expect(swal).toBeCalledWith({ type: 'success', text: '0' });
+ expect(toastr.info).toBeCalledWith('skinlib.redirecting');
+});
diff --git a/resources/lang/en/front-end.yml b/resources/lang/en/front-end.yml
index fcfacd3e..28e2ebcd 100644
--- a/resources/lang/en/front-end.yml
+++ b/resources/lang/en/front-end.yml
@@ -63,13 +63,22 @@ skinlib:
emptyUploadFile: You have not uploaded any file.
encodingError: 'Error: Encoding of this file is not accepted.'
fileExtError: 'Error: Textures should be PNG files.'
- upload: Upload
uploading: Uploading
redirecting: Redirecting...
setAsPrivate: Set as Private
setAsPublic: Set as Public
setPublicNotice: Sure to set this as public texture?
deleteNotice: Are you sure to delete this texture?
+ upload:
+ texture-name: Texture Name
+ texture-type: Texture Type
+ select-file: Select File
+ privacy-notice: Prevent it from being visible at skin library.
+ set-as-private: Make it Private
+ button: Upload
+ dropZone: Drop a file here
+ remove: Remove
+ cost: (It cost you about :score score)
user:
signRemainingTime: 'Available after :time :unit'
diff --git a/resources/lang/zh_CN/front-end.yml b/resources/lang/zh_CN/front-end.yml
index 8b7c78d3..cd41ed88 100644
--- a/resources/lang/zh_CN/front-end.yml
+++ b/resources/lang/zh_CN/front-end.yml
@@ -65,13 +65,22 @@ skinlib:
emptyUploadFile: 你还没有上传任何文件哦
encodingError: 错误:这张图片编码不对哦
fileExtError: 错误:皮肤文件必须为 PNG 格式
- upload: 确认上传
uploading: 上传中
redirecting: 正在跳转...
setAsPrivate: 设为隐私
setAsPublic: 设为公开
setPublicNotice: 要将此材质设置为公开吗?
deleteNotice: 真的要删除此材质吗?
+ upload:
+ texture-name: 材质名称
+ texture-type: 材质类型
+ select-file: 选择文件
+ privacy-notice: 其他人将不会在皮肤库中看到此材质
+ set-as-private: 设置为私密材质
+ button: 确认上传
+ dropZone: 拖拽文件到这里
+ remove: 移除
+ cost: (这会消耗您约 :score 积分)
user:
signRemainingTime: ':time :unit 后可签到'
@@ -290,86 +299,6 @@ general:
last-modified: 修改时间
vendor:
- fileinput:
- fileSingle: 文件
- filePlural: 个文件
- browseLabel: 选择 …
- removeLabel: 移除
- removeTitle: 清除选中文件
- cancelLabel: 取消
- cancelTitle: 取消进行中的上传
- uploadLabel: 上传
- uploadTitle: 上传选中文件
- msgNo: 没有
- msgNoFilesSelected: ''
- msgCancelled: 取消
- msgPlaceholder: '选择 {files}...'
- msgZoomModalHeading: 详细预览
- msgFileRequired: 必须选择一个文件上传.
- msgSizeTooSmall: '文件 "{name}" ({size} KB) 必须大于限定大小 {minSize} KB.'
- msgSizeTooLarge: '文件 "{name}" ({size} KB) 超过了允许大小 {maxSize} KB.'
- msgFilesTooLess: '你必须选择最少 {n} {files} 来上传. '
- msgFilesTooMany: '选择的上传文件个数 ({n}) 超出最大文件的限制个数 {m}.'
- msgFileNotFound: '文件 "{name}" 未找到!'
- msgFileSecured: '安全限制,为了防止读取文件 "{name}".'
- msgFileNotReadable: '文件 "{name}" 不可读.'
- msgFilePreviewAborted: '取消 "{name}" 的预览.'
- msgFilePreviewError: '读取 "{name}" 时出现了一个错误.'
- msgInvalidFileName: '文件名 "{name}" 包含非法字符.'
- msgInvalidFileType: '不正确的类型 "{name}". 只支持 "{types}" 类型的文件.'
- msgInvalidFileExtension: '不正确的文件扩展名 "{name}". 只支持 "{extensions}" 的文件扩展名.'
- msgFileTypes:
- image: image
- html: HTML
- text: text
- video: video
- audio: audio
- flash: flash
- pdf: PDF
- object: object
- msgUploadAborted: 该文件上传被中止
- msgUploadThreshold: 处理中...
- msgUploadBegin: 正在初始化...
- msgUploadEnd: 完成
- msgUploadEmpty: 无效的文件上传.
- msgUploadError: Error
- msgValidationError: 验证错误
- msgLoading: '加载第 {index} 文件 共 {files} …'
- msgProgress: '加载第 {index} 文件 共 {files} - {name} - {percent}% 完成.'
- msgSelected: '{n} {files} 选中'
- msgFoldersNotAllowed: '只支持拖拽文件! 跳过 {n} 拖拽的文件夹.'
- msgImageWidthSmall: '图像文件的"{name}"的宽度必须是至少{size}像素.'
- msgImageHeightSmall: '图像文件的"{name}"的高度必须至少为{size}像素.'
- msgImageWidthLarge: '图像文件"{name}"的宽度不能超过{size}像素.'
- msgImageHeightLarge: '图像文件"{name}"的高度不能超过{size}像素.'
- msgImageResizeError: 无法获取的图像尺寸调整。
- msgImageResizeException: '调整图像大小时发生错误。
{errors}'
- msgAjaxError: '{operation} 发生错误. 请重试!'
- msgAjaxProgressError: '{operation} 失败'
- ajaxOperations:
- deleteThumb: 删除文件
- uploadThumb: 上传文件
- uploadBatch: 批量上传
- uploadExtra: 表单数据上传
- dropZoneTitle: 拖拽文件到这里 …
支持多文件同时上传
- dropZoneClickTitle: '
(或点击{files}按钮选择文件)'
- fileActionSettings:
- removeTitle: 删除文件
- uploadTitle: 上传文件
- uploadRetryTitle: Retry upload
- zoomTitle: 查看详情
- dragTitle: 移动 / 重置
- indicatorNewTitle: 没有上传
- indicatorSuccessTitle: 上传
- indicatorErrorTitle: 上传错误
- indicatorLoadingTitle: 上传 ...
- previewZoomButtonTitles:
- prev: 预览上一个文件
- next: 预览下一个文件
- toggleheader: 缩放
- fullscreen: 全屏
- borderless: 无边界模式
- close: 关闭当前预览
datatable:
search: 搜索
rowsPerPage: 每页的项目数
diff --git a/resources/views/skinlib/upload.tpl b/resources/views/skinlib/upload.tpl
index 8ae88d4f..be764bf8 100644
--- a/resources/views/skinlib/upload.tpl
+++ b/resources/views/skinlib/upload.tpl
@@ -2,15 +2,6 @@
@section('title', trans('skinlib.upload.title'))
-@section('style')
-
-@endsection
-
@section('content')
@@ -23,83 +14,16 @@
-
-
-
-
-
-
-
- @php
- $regexp = option('texture_name_regexp');
- @endphp
-
-
-
-
-
-
-
-
-
-
-
-
@lang('skinlib.upload.private-score-notice', ['score' => option('private_score_per_storage')])
-
-
-
-
-
-
-
-
- @include('common.texture-preview')
-
-
-
-
-
+
-@endsection
-@section('script')
@endsection
diff --git a/webpack.config.js b/webpack.config.js
index c020a393..93029c4e 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -16,7 +16,6 @@ const config = {
style: [
'bootstrap/dist/css/bootstrap.min.css',
'admin-lte/dist/css/AdminLTE.min.css',
- 'bootstrap-fileinput/css/fileinput.min.css',
'@fortawesome/fontawesome-free/css/fontawesome.min.css',
'@fortawesome/fontawesome-free/css/regular.min.css',
'@fortawesome/fontawesome-free/css/solid.min.css',
diff --git a/yarn.lock b/yarn.lock
index 9ed461c9..d6fcde26 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1535,13 +1535,6 @@ bootstrap-daterangepicker@^2.1.25:
jquery ">=1.10"
moment "^2.9.0"
-bootstrap-fileinput@^4.4.7:
- version "4.4.7"
- resolved "https://registry.yarnpkg.com/bootstrap-fileinput/-/bootstrap-fileinput-4.4.7.tgz#ea6df7f6919e8de777ab7900fd07eed0893163ef"
- dependencies:
- bootstrap ">= 3.0.0"
- jquery ">= 1.9.0"
-
bootstrap-slider@^9.8.0:
version "9.10.0"
resolved "https://registry.yarnpkg.com/bootstrap-slider/-/bootstrap-slider-9.10.0.tgz#1103d6bc00cfbfa8cfc9a2599ab518c55643da3f"
@@ -1550,10 +1543,6 @@ bootstrap-timepicker@^0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/bootstrap-timepicker/-/bootstrap-timepicker-0.5.2.tgz#10ed9f2a2f0b8ccaefcde0fcf6a0738b919a3835"
-"bootstrap@>= 3.0.0":
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.0.0.tgz#ceb03842c145fcc1b9b4e15da2a05656ba68469a"
-
bootstrap@^3.3.7:
version "3.3.7"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-3.3.7.tgz#5a389394549f23330875a3b150656574f8a9eb71"
@@ -4978,7 +4967,7 @@ jquery-ui@^1.12.1:
version "1.12.1"
resolved "https://registry.yarnpkg.com/jquery-ui/-/jquery-ui-1.12.1.tgz#bcb4045c8dd0539c134bc1488cdd3e768a7a9e51"
-"jquery@2 - 3", "jquery@>= 1.9.0", jquery@>=1.10, jquery@>=1.12.0, jquery@>=1.5, jquery@>=1.7, "jquery@>=1.7.1 <4.0.0", jquery@>=1.8, jquery@^3.2.1, jquery@^3.3.1:
+"jquery@2 - 3", jquery@>=1.10, jquery@>=1.12.0, jquery@>=1.5, jquery@>=1.7, "jquery@>=1.7.1 <4.0.0", jquery@>=1.8, jquery@^3.2.1, jquery@^3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.3.1.tgz#958ce29e81c9790f31be7792df5d4d95fc57fbca"
@@ -8481,6 +8470,10 @@ vue-template-es2015-compiler@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.6.0.tgz#dc42697133302ce3017524356a6c61b7b69b4a18"
+vue-upload-component@^2.8.11:
+ version "2.8.11"
+ resolved "https://registry.npmjs.org/vue-upload-component/-/vue-upload-component-2.8.11.tgz#7dd207144ba9502760aecf77d42e4ab072b94b07"
+
vue@^2.5.16:
version "2.5.16"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.16.tgz#07edb75e8412aaeed871ebafa99f4672584a0085"