Selaa lähdekoodia

优化 upload 模块,添加响应式处理

icssoa 4 vuotta sitten
vanhempi
sitoutus
1c9f7caeaf

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
 	"name": "cool-admin-vue",
-	"version": "3.1.7",
+	"version": "3.2.0",
 	"scripts": {
 		"serve": "vue-cli-service serve",
 		"build": "vue-cli-service build",

+ 1 - 1
src/cool/modules/base/store/process.js

@@ -9,7 +9,7 @@ export default {
 		list: [fMenu]
 	},
 	getters: {
-		// 窗口列表
+		// 页面进程列表
 		processList: state => state.list
 	},
 	mutations: {

+ 6 - 17
src/cool/modules/base/views/user.vue

@@ -22,7 +22,7 @@
 				</div>
 
 				<div class="container">
-					<cl-crud ref="crud" @load="onLoad" :on-refresh="onRefresh">
+					<cl-crud ref="crud" :on-refresh="onRefresh" @load="onLoad">
 						<el-row type="flex">
 							<cl-refresh-btn></cl-refresh-btn>
 							<cl-add-btn></cl-add-btn>
@@ -123,36 +123,30 @@ export default {
 				columns: [
 					{
 						type: "selection",
-						align: "center",
-						width: "60"
+						width: 60
 					},
 					{
 						prop: "headImg",
-						label: "头像",
-						align: "center"
+						label: "头像"
 					},
 					{
 						prop: "name",
 						label: "姓名",
-						align: "center",
 						"min-width": 150
 					},
 					{
 						prop: "username",
 						label: "用户名",
-						align: "center",
 						"min-width": 150
 					},
 					{
 						prop: "nickName",
 						label: "昵称",
-						align: "center",
 						"min-width": 150
 					},
 					{
 						prop: "departmentName",
 						label: "部门名称",
-						align: "center",
 						"min-width": 150
 					},
 					{
@@ -164,19 +158,16 @@ export default {
 					{
 						prop: "phone",
 						label: "手机号码",
-						align: "center",
 						"min-width": 150
 					},
 					{
 						prop: "remark",
 						label: "备注",
-						align: "center",
 						"min-width": 150
 					},
 					{
 						prop: "status",
 						label: "状态",
-						align: "center",
 						"min-width": 120,
 						dict: [
 							{
@@ -194,15 +185,13 @@ export default {
 					{
 						prop: "createTime",
 						label: "创建时间",
-						align: "center",
 						sortable: "custom",
 						"min-width": 150
 					},
 					{
-						align: "center",
 						type: "op",
 						buttons: ["slot-move-btn", "edit", "delete"],
-						width: "160px"
+						width: 160
 					}
 				]
 			},
@@ -271,7 +260,7 @@ export default {
 						prop: "password",
 						label: "密码",
 						span: 12,
-						hidden: ":isEdit",
+						hidden: ":isAdd",
 						component: {
 							name: "el-input",
 							attrs: {
@@ -362,7 +351,7 @@ export default {
 					},
 					{
 						prop: "tips",
-						hidden: ":isAdd",
+						hidden: ":isEdit",
 						component: (
 							<div>
 								<i class="el-icon-warning"></i>

+ 3 - 2
src/cool/modules/chat/components/chat.vue

@@ -12,7 +12,7 @@
 				'append-to-body': true,
 				'close-on-click-modal': false
 			}"
-			:controls="['slot-session', 'cl-flex1', 'fullscreen', 'close']"
+			:controls="['slot-expand', 'cl-flex1', 'fullscreen', 'close']"
 		>
 			<div class="cl-chat">
 				<!-- 会话列表 -->
@@ -29,7 +29,8 @@
 				</div>
 			</div>
 
-			<template #slot-session>
+			<!-- 展开按钮 -->
+			<template #slot-expand>
 				<button v-if="session">
 					<i
 						class="el-icon-notebook-2"

+ 1 - 1
src/cool/modules/upload/components/index.js

@@ -1,5 +1,5 @@
 import Upload from "./index.vue";
-import UploadSpace from "./space.vue";
+import UploadSpace from "./space/index.vue";
 
 export default {
 	Upload,

+ 0 - 721
src/cool/modules/upload/components/space.vue

@@ -1,721 +0,0 @@
-<template>
-	<div class="cl-upload-space__wrap">
-		<slot>
-			<el-button v-if="showButton" size="mini" @click="open">点击上传</el-button>
-		</slot>
-
-		<!-- 弹框 -->
-		<cl-dialog :visible.sync="visible" v-bind="props" :op-list="['close']">
-			<div class="cl-upload-space">
-				<!-- 类目 -->
-				<div class="cl-upload-space__category">
-					<div class="cl-upload-space__category-search">
-						<el-button type="primary" size="mini" @click="editCategory()"
-							>添加分类</el-button
-						>
-
-						<el-input
-							v-model="category.keyword"
-							placeholder="输入关键字过滤"
-							size="mini"
-						></el-input>
-					</div>
-
-					<div class="cl-upload-space__category-list">
-						<ul>
-							<li
-								v-for="(item, index) in categoryList"
-								:key="index"
-								:class="{
-									'is-active': item.id == category.current.id
-								}"
-								@click="selectCategory(item)"
-								@contextmenu.stop.prevent="openCategoryContextMenu($event, item)"
-							>
-								{{ item.name }}
-							</li>
-						</ul>
-					</div>
-				</div>
-
-				<!-- 内容 -->
-				<div class="cl-upload-space__content">
-					<!-- 操作栏 -->
-					<div class="cl-upload-space__opbar">
-						<el-button
-							type="success"
-							size="mini"
-							:disabled="selection.length === 0"
-							@click="confirmFile()"
-							>使用选中文件</el-button
-						>
-
-						<el-button
-							type="danger"
-							size="mini"
-							:disabled="selection.length === 0"
-							@click="deleteFile()"
-							>删除选中文件</el-button
-						>
-
-						<cl-upload
-							style="margin-left: 10px"
-							list-type="slot"
-							:action="action"
-							:accept="accept"
-							:limit-size="limitSize"
-							:show-file-list="false"
-							:headers="headers"
-							:data="data"
-							:disabled="disabled"
-							:rename="rename"
-							:on-success="onSuccess"
-							:on-progress="onProgress"
-							:before-upload="beforeUpload"
-						>
-							<el-button size="mini" type="primary">点击上传</el-button>
-						</cl-upload>
-					</div>
-
-					<!-- 文件区域 -->
-					<div
-						class="cl-upload-space__file"
-						v-loading="file.loading"
-						element-loading-text="拼命加载中"
-					>
-						<!-- 文件列表 -->
-						<el-row v-if="file.list.length > 0">
-							<el-col :span="6" v-for="item in file.list" :key="item.id">
-								<file-item
-									:value="item"
-									:element-loading-text="item.progress"
-									v-loading="item.loading"
-								></file-item>
-							</el-col>
-						</el-row>
-
-						<!-- 空态 -->
-						<div class="cl-upload-space__file-empty" v-else>
-							<cl-upload
-								drag
-								:action="action"
-								:accept="accept"
-								:limit-size="limitSize"
-								:headers="headers"
-								:data="data"
-								:disabled="disabled"
-								:rename="rename"
-								:on-success="onSuccess"
-								:on-progress="onProgress"
-								:before-upload="beforeUpload"
-							>
-								<i class="el-icon-upload"></i>
-								<div class="el-upload__text">
-									将文件拖到此处,或<em>点击上传</em>
-								</div>
-							</cl-upload>
-						</div>
-					</div>
-
-					<!-- 分页 -->
-					<el-pagination
-						background
-						:page-size="file.pagination.size"
-						:current-page="file.pagination.page"
-						:total="file.pagination.total"
-						@current-change="onCurrentChange"
-					></el-pagination>
-				</div>
-			</div>
-		</cl-dialog>
-	</div>
-</template>
-
-<script>
-import { mapGetters } from "vuex";
-import { last, isEmpty } from "cl-admin/utils";
-
-export default {
-	name: "cl-upload-space",
-
-	componentName: "UploadSpace",
-
-	props: {
-		// 上传的地址
-		action: String,
-		// 选择图片的长度
-		limit: {
-			type: Number,
-			default: 8
-		},
-		// 最大允许上传文件大小(MB)
-		limitSize: {
-			type: Number,
-			default: 10
-		},
-		// 是否禁用
-		disabled: Boolean,
-		// 是否以 uuid 重命名
-		rename: Boolean,
-		// 设置上传的请求头部
-		headers: Object,
-		// 上传时附带的额外参数
-		data: Object,
-		// 上传的文件类型
-		accept: String,
-		// 是否返回详细数据
-		detailData: Boolean,
-		// 是否显示按钮
-		showButton: {
-			type: Boolean,
-			default: true
-		}
-	},
-
-	components: {
-		fileItem: {
-			props: {
-				value: Object
-			},
-
-			computed: {
-				parent() {
-					let parent = this;
-
-					while (parent.$options.componentName != "UploadSpace") {
-						parent = parent.$parent;
-					}
-
-					return parent;
-				}
-			},
-
-			methods: {
-				onSelect() {
-					this.parent.selectFile(this.value);
-				},
-
-				onContextMenu(e) {
-					this.parent.openFileContextMenu(e, this.value);
-					e.stopPropagation();
-					e.preventDefault();
-				}
-			},
-
-			render() {
-				if (!this.value) {
-					return null;
-				}
-
-				let itemEl = null;
-
-				const { url, type, selected, id } = this.value;
-				const fileType = (type || "").split("/")[0];
-
-				switch (fileType) {
-					case "image":
-						itemEl = <el-image fit="cover" src={url} lazy></el-image>;
-						break;
-
-					case "video":
-						itemEl = (
-							<video
-								controls
-								src={url}
-								style={{
-									"max-height": "100%",
-									"max-width": "100%"
-								}}></video>
-						);
-						break;
-
-					default:
-						itemEl = <span>{url}</span>;
-						break;
-				}
-
-				return (
-					<div
-						class={["cl-upload-space__file-item", `is-${fileType}`]}
-						on-click={this.onSelect}
-						on-contextmenu={this.onContextMenu}>
-						{itemEl}
-
-						<div class="cl-upload-space__file-size"></div>
-
-						{selected && (
-							<div class="cl-upload-space__file-mask">
-								<i class="el-icon-success"></i>
-							</div>
-						)}
-					</div>
-				);
-			}
-		}
-	},
-
-	data() {
-		return {
-			visible: false,
-			props: {
-				title: "文件空间",
-				props: {
-					"close-on-click-modal": false,
-					"append-to-body": true,
-					width: "1000px"
-				}
-			},
-			category: {
-				list: [],
-				current: {},
-				keyword: ""
-			},
-			file: {
-				list: [],
-				pagination: {
-					page: 1,
-					size: 12,
-					total: 0
-				},
-				loading: false
-			}
-		};
-	},
-
-	computed: {
-		...mapGetters(["token"]),
-
-		categoryList() {
-			return this.category.list.filter(e => e.name.includes(this.category.keyword));
-		},
-
-		selection() {
-			return this.file.list.filter(e => e.selected);
-		}
-	},
-
-	filters: {
-		file_name(url) {
-			return last(url.split("."));
-		}
-	},
-
-	created() {
-		this.refreshCategory().then(() => {
-			this.category.current = this.category.list[0];
-			this.refreshFile();
-		});
-	},
-
-	methods: {
-		open(key) {
-			this.visible = true;
-		},
-
-		close() {
-			this.visible = false;
-
-			this.$nextTick(() => {
-				this.file.list.map(e => {
-					this.$set(e, "selected", false);
-				});
-			});
-		},
-
-		// 上传成功
-		onSuccess(res, file) {
-			let item = this.file.list.find(e => file.uid == e.uid);
-
-			if (item) {
-				item.url = res.data;
-
-				this.$service.space.info
-					.add({
-						url: res.data,
-						type: item.type,
-						classifyId: item.classifyId
-					})
-					.then(res => {
-						item.loading = false;
-						item.id = res.id;
-					})
-					.catch(err => {
-						this.$message.error(err);
-					});
-			}
-		},
-
-		// 上传前,添加文件
-		beforeUpload({ tempFilePath, type, uid }) {
-			this.file.list.unshift({
-				url: tempFilePath,
-				type,
-				uid,
-				classifyId: this.category.current.id,
-				loading: true,
-				progress: "0%"
-			});
-		},
-
-		// 上传进度
-		onProgress({ percent }, file) {
-			let item = this.file.list.find(({ uid }) => uid == file.uid);
-
-			if (item) {
-				item.progress = percent + "%";
-			}
-		},
-
-		// 刷新资源文件
-		refreshFile(params) {
-			this.file.loading = true;
-
-			this.$service.space.info
-				.page({
-					...this.file.pagination,
-					...params,
-					classifyId: this.category.current.id,
-					type: this.accept
-				})
-				.then(res => {
-					this.file.pagination = res.pagination;
-					this.file.list = res.list;
-				})
-				.done(() => {
-					this.file.loading = false;
-				});
-		},
-
-		// 刷新分类
-		refreshCategory() {
-			return this.$service.space.type.list().then(res => {
-				res.unshift({
-					name: "全部文件",
-					id: null
-				});
-				this.category.list = res;
-			});
-		},
-
-		// 编辑分类
-		editCategory(item = {}) {
-			this.$crud.openForm({
-				title: "添加分类",
-				width: "400px",
-				items: [
-					{
-						label: "分类名称",
-						prop: "name",
-						value: item.name,
-						component: {
-							name: "el-input",
-							attrs: {
-								placeholder: "请填写分类名称"
-							}
-						},
-						rules: {
-							required: true,
-							message: "分类名称不能为空"
-						}
-					}
-				],
-				on: {
-					submit: (data, { done, close }) => {
-						let next = null;
-
-						if (!item.id) {
-							next = this.$service.space.type.add(data);
-						} else {
-							next = this.$service.space.type.update({
-								...data,
-								id: item.id
-							});
-						}
-
-						next.then(() => {
-							this.refreshCategory();
-							close();
-						}).catch(err => {
-							this.$message.error(err);
-							done();
-						});
-					}
-				}
-			});
-		},
-
-		// 选择类目
-		selectCategory(item) {
-			this.category.current = item;
-			this.file.pagination = {
-				page: 1,
-				size: 12,
-				total: 0
-			};
-			this.refreshFile({
-				classifyId: item.id
-			});
-		},
-
-		// 打开类目列表右键菜单
-		openCategoryContextMenu(e, { id, name }) {
-			if (!id) {
-				return false;
-			}
-			this.$crud.openContextMenu(e, {
-				list: [
-					{
-						label: "编辑",
-						"suffix-icon": "el-icon-edit",
-						callback: (_, done) => {
-							done();
-							this.editCategory({ id, name });
-						}
-					},
-					{
-						label: "删除",
-						"suffix-icon": "el-icon-delete",
-						callback: (_, done) => {
-							done();
-
-							this.$confirm(`此操作将删除【${name}】下的文件, 是否继续?`, "提示", {
-								type: "warning"
-							})
-								.then(() => {
-									this.$service.space.type
-										.delete({
-											ids: id
-										})
-										.then(() => {
-											this.$message.success("删除成功");
-											this.refreshCategory();
-
-											// 删除当前类目时,重置选择
-											if (id == this.category.current.id) {
-												this.category.current = this.category.list[0];
-												this.refreshFile();
-											}
-										})
-										.catch(err => {
-											console.error(err);
-											this.$message.error(err);
-										});
-								})
-								.catch(() => {});
-						}
-					}
-				]
-			});
-		},
-
-		// 打开文件列表右键菜单
-		openFileContextMenu(e, data) {
-			this.$crud.openContextMenu(e, {
-				list: [
-					{
-						label: data.selected ? "取消选中" : "选中",
-						"suffix-icon": data.selected ? "el-icon-close" : "el-icon-check",
-						callback: (_, done) => {
-							this.selectFile(data);
-							done();
-						}
-					},
-					{
-						label: "删除",
-						"suffix-icon": "el-icon-delete",
-						callback: (_, done) => {
-							this.deleteFile(data);
-							done();
-						}
-					}
-				]
-			});
-		},
-
-		// 确认选中文件
-		confirmFile() {
-			const selection = this.selection.filter((e, i) => i < this.limit);
-			const urls = selection.map(e => e.url).join(",");
-
-			this.$emit("input", urls);
-			this.$emit("confirm", this.detailData ? selection : urls);
-
-			this.close();
-		},
-
-		// 选择文件
-		selectFile(item) {
-			this.$set(item, "selected", !item.selected);
-		},
-
-		// 删除选中文件
-		deleteFile(...selection) {
-			if (isEmpty(selection)) {
-				selection = this.selection;
-			}
-
-			this.$confirm("此操作将删除文件, 是否继续?", "提示", {
-				type: "warning"
-			})
-				.then(() => {
-					this.$message.success("删除成功");
-
-					this.file.list = this.file.list.filter(
-						e => !selection.map(e => e.id).includes(e.id)
-					);
-
-					this.$service.space.info
-						.delete({
-							ids: selection.map(e => e.id).join(",")
-						})
-						.catch(err => {
-							this.$message.error(err);
-						});
-				})
-				.catch(() => {});
-		},
-
-		// 选择页
-		onCurrentChange(i) {
-			this.refreshFile({
-				page: i
-			});
-		}
-	}
-};
-</script>
-
-<style lang="scss" scoped>
-.cl-upload-space {
-	display: flex;
-	min-height: 520px;
-
-	&__category {
-		width: 250px;
-		margin-right: 20px;
-
-		&-search {
-			display: flex;
-			align-items: center;
-			margin-bottom: 5px;
-
-			.el-button {
-				margin-right: 10px;
-			}
-		}
-
-		&-list {
-			overflow: hidden auto;
-
-			ul {
-				li {
-					list-style: none;
-					font-size: 14px;
-					height: 40px;
-					line-height: 40px;
-					border-bottom: 1px dashed #eee;
-					padding: 0 5px;
-					cursor: pointer;
-
-					&.is-active {
-						color: #409eff;
-					}
-				}
-			}
-		}
-	}
-
-	&__content {
-		flex: 1;
-	}
-
-	&__opbar {
-		display: flex;
-		align-items: center;
-		margin-bottom: 10px;
-	}
-
-	&__file {
-		height: calc(100% - 80px);
-		overflow: hidden auto;
-		margin-bottom: 10px;
-
-		/deep/.cl-upload-space__file-item {
-			display: flex;
-			align-items: center;
-			justify-content: center;
-			height: 160px;
-			width: 160px;
-			cursor: pointer;
-			position: relative;
-			border-radius: 3px;
-			box-sizing: border-box;
-			border: 1px solid #eee;
-			margin: 5px 0;
-
-			&.is-image {
-				overflow: hidden;
-
-				img {
-					height: 100%;
-					width: 100%;
-				}
-			}
-
-			&.is-video {
-				video {
-					max-height: 100%;
-					width: 100%;
-				}
-			}
-
-			.cl-upload-space__file-size {
-				position: absolute;
-				bottom: 0;
-				left: 0;
-				background-color: rgba(0, 0, 0, 0.5);
-			}
-
-			.cl-upload-space__file-mask {
-				position: absolute;
-				left: 0;
-				top: 0;
-				height: 100%;
-				width: 100%;
-				background-color: rgba(0, 0, 0, 0.5);
-				display: flex;
-				justify-content: center;
-				align-items: center;
-
-				i {
-					font-size: 30px;
-					color: #67c23a;
-				}
-			}
-		}
-
-		&-empty {
-			display: flex;
-			align-items: center;
-			justify-content: center;
-			margin-top: 100px;
-
-			& > div {
-				display: flex;
-				flex-direction: column;
-				justify-content: center;
-				align-items: center;
-				border-radius: 6px;
-				cursor: pointer;
-				height: 180px;
-				width: 360px;
-
-				i {
-					font-size: 67px;
-					color: #c0c4cc;
-				}
-			}
-		}
-	}
-}
-</style>

+ 275 - 0
src/cool/modules/upload/components/space/category.vue

@@ -0,0 +1,275 @@
+<template>
+	<div
+		class="cl-upload-space-category"
+		:class="{
+			'is-position': browser.isMini,
+			'is-show': space.category.visible
+		}"
+	>
+		<div class="cl-upload-space-category__search">
+			<el-button type="primary" size="mini" @click="edit()">添加分类</el-button>
+
+			<el-input v-model="keyword" placeholder="输入关键字过滤" size="mini"></el-input>
+		</div>
+
+		<div class="cl-upload-space-category__list">
+			<ul class="scroller1">
+				<li
+					v-for="(item, index) in flist"
+					:key="index"
+					:class="{
+						'is-active': item.id == current
+					}"
+					@click="select(item.id)"
+					@contextmenu.stop.prevent="openContextMenu($event, item)"
+				>
+					{{ item.name }}
+				</li>
+			</ul>
+		</div>
+	</div>
+</template>
+
+<script>
+import { mapGetters } from "vuex";
+import { isEmpty } from "cl-admin/utils";
+
+export default {
+	name: "cl-upload-space-category",
+
+	props: {
+		value: [Number]
+	},
+
+	inject: ["space"],
+
+	data() {
+		return {
+			list: [],
+			current: undefined,
+			keyword: ""
+		};
+	},
+
+	computed: {
+		...mapGetters(["browser"]),
+
+		flist() {
+			return this.list.filter(e => e.name.includes(this.keyword));
+		}
+	},
+
+	watch: {
+		current: {
+			handler(id) {
+				this.$emit("input", id);
+				this.$emit("change", id);
+			}
+		}
+	},
+
+	created() {
+		this.refresh();
+	},
+
+	methods: {
+		// 刷新分类
+		refresh() {
+			return this.$service.space.type.list().then(res => {
+				res.unshift({
+					name: "全部文件",
+					id: null
+				});
+
+				this.list = res;
+
+				if (!isEmpty(res)) {
+					if (!this.current) {
+						this.current = res[0].id;
+					}
+				}
+			});
+		},
+
+		// 编辑分类
+		edit(item = {}) {
+			this.$crud.openForm({
+				title: "添加分类",
+				width: "400px",
+				items: [
+					{
+						label: "分类名称",
+						prop: "name",
+						value: item.name,
+						component: {
+							name: "el-input",
+							attrs: {
+								placeholder: "请填写分类名称"
+							}
+						},
+						rules: {
+							required: true,
+							message: "分类名称不能为空"
+						}
+					}
+				],
+				on: {
+					submit: (data, { done, close }) => {
+						let next = null;
+
+						if (!item.id) {
+							next = this.$service.space.type.add(data);
+						} else {
+							next = this.$service.space.type.update({
+								...data,
+								id: item.id
+							});
+						}
+
+						next.then(() => {
+							this.refresh();
+							close();
+						}).catch(err => {
+							this.$message.error(err);
+							done();
+						});
+					}
+				}
+			});
+		},
+
+		// 选择类目
+		select(id) {
+			this.current = id;
+
+			// 小屏幕下收起左侧类目
+			if (this.browser.isMini) {
+				this.space.category.visible = false;
+			}
+		},
+
+		// 打开类目列表右键菜单
+		openContextMenu(e, { id, name }) {
+			if (!id) {
+				return false;
+			}
+
+			this.$crud.openContextMenu(e, {
+				list: [
+					{
+						label: "刷新",
+						"suffix-icon": "el-icon-edit",
+						callback: (_, done) => {
+							this.refresh();
+							done();
+						}
+					},
+					{
+						label: "编辑",
+						"suffix-icon": "el-icon-edit",
+						callback: (_, done) => {
+							this.edit({ id, name });
+							done();
+						}
+					},
+					{
+						label: "删除",
+						"suffix-icon": "el-icon-delete",
+						callback: (_, done) => {
+							this.$confirm(`此操作将删除【${name}】下的文件, 是否继续?`, "提示", {
+								type: "warning"
+							})
+								.then(() => {
+									this.$service.space.type
+										.delete({
+											ids: [id]
+										})
+										.then(() => {
+											this.$message.success("删除成功");
+
+											if (id == this.current) {
+												this.current = null;
+											}
+
+											this.refresh();
+										})
+										.catch(err => {
+											this.$message.error(err);
+										});
+								})
+								.catch(() => null);
+
+							done();
+						}
+					}
+				]
+			});
+		}
+	}
+};
+</script>
+
+<style lang="scss" scoped>
+.cl-upload-space-category {
+	height: 100%;
+	width: 0;
+	background-color: #fff;
+	overflow: hidden;
+	transition: width 0.2s ease-in-out;
+	border-radius: 5px;
+
+	&.is-show {
+		width: 250px;
+		margin-right: 5px;
+	}
+
+	&.is-position {
+		position: absolute;
+		left: 5px;
+		top: 51px;
+		height: calc(100% - 56px);
+		z-index: 3000;
+
+		&.is-show {
+			width: calc(100% - 10px);
+		}
+	}
+
+	&__search {
+		display: flex;
+		align-items: center;
+		padding: 10px;
+
+		.el-button {
+			margin-right: 10px;
+		}
+	}
+
+	&__list {
+		height: calc(100% - 48px);
+		padding: 0 10px;
+
+		ul {
+			height: 100%;
+
+			li {
+				list-style: none;
+				font-size: 14px;
+				height: 40px;
+				line-height: 40px;
+				border-bottom: 1px dashed #eee;
+				padding: 0 10px;
+				cursor: pointer;
+
+				&.is-active {
+					color: $color-primary;
+				}
+
+				&:hover {
+					background-color: #f7f7f7;
+				}
+			}
+		}
+	}
+}
+</style>

+ 167 - 0
src/cool/modules/upload/components/space/file-item.vue

@@ -0,0 +1,167 @@
+<template>
+	<div
+		class="cl-upload-space-item"
+		:class="[`is-${type}`]"
+		@click.stop.prevent="select"
+		@contextmenu.stop.prevent="openContextMenu"
+	>
+		<!-- 错误 -->
+		<template v-if="value.error">
+			<div class="cl-upload-space-item__error">上传失败:{{ value.error }}</div>
+		</template>
+
+		<!-- 成功 -->
+		<template v-else>
+			<!-- 图片 -->
+			<template v-if="type === 'image'">
+				<el-image fit="cover" :src="value.url" lazy></el-image>
+			</template>
+
+			<!-- 视频 -->
+			<template v-else-if="type === 'video'">
+				<video
+					controls
+					:src="value.url"
+					:style="{
+						'max-height': '100%',
+						'max-width': '100%'
+					}"
+				></video>
+			</template>
+
+			<!-- 其他 -->
+			<template v-else>
+				<span>{{ value.url }}</span>
+			</template>
+		</template>
+
+		<!-- 大小 -->
+		<div class="cl-upload-space-item__size"></div>
+
+		<!-- 遮罩层 -->
+		<div class="cl-upload-space-item__mask" v-if="isSelected">
+			<span>{{ index + 1 }}</span>
+		</div>
+	</div>
+</template>
+
+<script>
+export default {
+	name: "cl-upload-space-item",
+
+	props: {
+		value: Object
+	},
+
+	inject: ["space"],
+
+	computed: {
+		index() {
+			return this.space.selection.findIndex(e => e.id === this.value.id);
+		},
+
+		isSelected() {
+			return this.index >= 0;
+		},
+
+		type() {
+			return (this.value.type || "").split("/")[0];
+		}
+	},
+
+	methods: {
+		select() {
+			this.$emit("select", this.value);
+		},
+
+		remove() {
+			this.$emit("remove", this.value);
+		},
+
+		openContextMenu(e) {
+			this.$crud.openContextMenu(e, {
+				list: [
+					{
+						label: this.isSelected ? "取消选中" : "选中",
+						"suffix-icon": this.isSelected ? "el-icon-close" : "el-icon-check",
+						callback: (_, done) => {
+							this.select();
+							done();
+						}
+					},
+					{
+						label: "删除",
+						"suffix-icon": "el-icon-delete",
+						callback: (_, done) => {
+							this.remove();
+							done();
+						}
+					}
+				]
+			});
+		}
+	}
+};
+</script>
+
+<style lang="scss" scoped>
+.cl-upload-space-item {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	height: 160px;
+	width: 160px;
+	cursor: pointer;
+	position: relative;
+	border-radius: 3px;
+	box-sizing: border-box;
+	border: 1px solid #eee;
+	margin: 5px 10px 5px 0;
+
+	&.is-image {
+		overflow: hidden;
+	}
+
+	&.is-video {
+		video {
+			max-height: 100%;
+			width: 100%;
+		}
+	}
+
+	&__size {
+		position: absolute;
+		bottom: 0;
+		left: 0;
+		background-color: rgba(0, 0, 0, 0.3);
+	}
+
+	&__error {
+		padding: 10px;
+		color: red;
+	}
+
+	&__mask {
+		position: absolute;
+		left: 0;
+		top: 0;
+		height: 100%;
+		width: 100%;
+		background-color: rgba(0, 0, 0, 0.3);
+
+		span {
+			position: absolute;
+			right: 10px;
+			top: 10px;
+			background-color: #67c23a;
+			color: #fff;
+			display: inline-block;
+			height: 20px;
+			width: 20px;
+			text-align: center;
+			line-height: 20px;
+			border-radius: 20px;
+		}
+	}
+}
+</style>

+ 462 - 0
src/cool/modules/upload/components/space/index.vue

@@ -0,0 +1,462 @@
+<template>
+	<div class="cl-upload-space__wrap">
+		<slot>
+			<el-button v-if="showButton" size="mini" @click="open">点击上传</el-button>
+		</slot>
+
+		<!-- 弹框 -->
+		<cl-dialog
+			title="文件空间"
+			height="630px"
+			width="1000px"
+			:visible.sync="visible"
+			:props="{
+				'close-on-click-modal': false,
+				'append-to-body': true,
+				customClass: 'dialog-upload-space'
+			}"
+			:controls="['slot-expand', 'cl-flex1', 'fullscreen', 'close']"
+		>
+			<div class="cl-upload-space">
+				<!-- 类目 -->
+				<category v-model="category.id" @change="refresh()" />
+
+				<!-- 内容 -->
+				<div class="cl-upload-space__content">
+					<!-- 操作栏 -->
+					<div class="cl-upload-space__header scroller1">
+						<el-button size="mini" @click="refresh()">刷新</el-button>
+
+						<cl-upload
+							style="margin: 0 10px"
+							list-type="slot"
+							:action="action"
+							:accept="accept"
+							:limit-size="limitSize"
+							:show-file-list="false"
+							:headers="headers"
+							:data="data"
+							:disabled="disabled"
+							:rename="rename"
+							:on-success="onSuccess"
+							:on-error="onError"
+							:on-progress="onProgress"
+							:before-upload="beforeUpload"
+						>
+							<el-button size="mini" type="primary">点击上传</el-button>
+						</cl-upload>
+
+						<el-button
+							type="success"
+							size="mini"
+							:disabled="!isSelected"
+							@click="confirm()"
+							>使用选中文件 {{ this.limitTip }}</el-button
+						>
+
+						<el-button
+							type="danger"
+							size="mini"
+							:disabled="!isSelected"
+							@click="remove()"
+							>删除选中文件</el-button
+						>
+					</div>
+
+					<!-- 文件区域 -->
+					<div
+						class="cl-upload-space__file scroller1"
+						v-loading="loading"
+						element-loading-text="拼命加载中"
+					>
+						<!-- 文件列表 -->
+						<template v-if="list.length > 0">
+							<div class="cl-upload-space__file-list">
+								<file-item
+									v-for="item in list"
+									:key="item.id"
+									:value="item"
+									:element-loading-text="item.progress"
+									v-loading="item.loading"
+									@select="select"
+									@remove="remove"
+								></file-item>
+							</div>
+						</template>
+
+						<!-- 空态 -->
+						<div class="cl-upload-space__file-empty" v-else>
+							<cl-upload
+								drag
+								:action="action"
+								:accept="accept"
+								:limit-size="limitSize"
+								:headers="headers"
+								:data="data"
+								:disabled="disabled"
+								:rename="rename"
+								:on-success="onSuccess"
+								:on-error="onError"
+								:on-progress="onProgress"
+								:before-upload="beforeUpload"
+							>
+								<i class="el-icon-upload"></i>
+								<div class="el-upload__text">
+									将文件拖到此处,或<em>点击上传</em>
+								</div>
+							</cl-upload>
+						</div>
+					</div>
+
+					<!-- 分页 -->
+					<div class="cl-upload-space__footer">
+						<el-pagination
+							background
+							:page-size="pagination.size"
+							:current-page="pagination.page"
+							:total="pagination.total"
+							@current-change="
+								page => {
+									refresh({ page });
+								}
+							"
+						></el-pagination>
+					</div>
+				</div>
+			</div>
+
+			<!-- 展开按钮 -->
+			<template #slot-expand>
+				<button>
+					<i
+						class="el-icon-notebook-2"
+						v-if="category.visible"
+						@click="category.visible = false"
+					></i>
+					<i class="el-icon-arrow-left" v-else @click="category.visible = true"></i>
+				</button>
+			</template>
+		</cl-dialog>
+	</div>
+</template>
+
+<script>
+import { isEmpty } from "cl-admin/utils";
+import Category from "./category";
+import FileItem from "./file-item";
+import { mapGetters } from "vuex";
+
+export default {
+	name: "cl-upload-space",
+
+	props: {
+		// 上传的地址
+		action: String,
+		// 选择图片的长度
+		limit: {
+			type: Number,
+			default: 9
+		},
+		// 最大允许上传文件大小(MB)
+		limitSize: {
+			type: Number,
+			default: 10
+		},
+		// 是否禁用
+		disabled: Boolean,
+		// 是否以 uuid 重命名
+		rename: Boolean,
+		// 设置上传的请求头部
+		headers: Object,
+		// 上传时附带的额外参数
+		data: Object,
+		// 上传的文件类型
+		accept: String,
+		// 是否返回详细数据
+		detailData: Boolean,
+		// 是否显示按钮
+		showButton: {
+			type: Boolean,
+			default: true
+		}
+	},
+
+	components: {
+		Category,
+		FileItem
+	},
+
+	provide() {
+		return {
+			space: this
+		};
+	},
+
+	data() {
+		return {
+			visible: true,
+			loading: false,
+			category: {
+				id: null,
+				visible: true
+			},
+			selection: [],
+			list: [],
+			pagination: {
+				page: 1,
+				size: 12,
+				total: 0
+			}
+		};
+	},
+
+	computed: {
+		...mapGetters(["browser"]),
+
+		limitTip() {
+			return this.selection.length + "/" + this.limit;
+		},
+
+		isSelected() {
+			return !isEmpty(this.selection);
+		}
+	},
+
+	watch: {
+		"browser.isMini": {
+			immediate: true,
+			handler(val) {
+				this.category.visible = val ? false : true;
+			}
+		}
+	},
+
+	methods: {
+		open() {
+			this.visible = true;
+		},
+
+		close() {
+			this.visible = false;
+			this.clear();
+		},
+
+		clear() {
+			this.selection = [];
+		},
+
+		// 上传成功
+		onSuccess(res, file) {
+			const item = this.list.find(e => file.uid == e.uid);
+
+			if (item) {
+				item.url = res.data;
+
+				this.$service.space.info
+					.add({
+						url: res.data,
+						type: item.type,
+						classifyId: item.classifyId
+					})
+					.then(res => {
+						item.loading = false;
+						item.id = res.id;
+					})
+					.catch(err => {
+						this.$message.error(err);
+					});
+			}
+		},
+
+		// 上传失败
+		onError(err, file) {
+			const item = this.list.find(e => file.uid == e.uid);
+
+			if (item) {
+				item.loading = false;
+				this.$set(item, "error", err);
+			}
+		},
+
+		// 上传前,添加文件
+		beforeUpload({ tempFilePath, type, uid }) {
+			this.list.unshift({
+				url: tempFilePath,
+				type,
+				uid,
+				classifyId: this.category.id,
+				loading: true,
+				progress: "0%"
+			});
+		},
+
+		// 上传进度
+		onProgress({ percent }, file) {
+			const item = this.list.find(({ uid }) => uid == file.uid);
+
+			if (item) {
+				item.progress = percent + "%";
+			}
+		},
+
+		// 刷新资源文件
+		refresh(params) {
+			// 清空选择
+			this.clear();
+
+			this.loading = true;
+
+			this.$service.space.info
+				.page({
+					...this.pagination,
+					...params,
+					classifyId: this.category.id,
+					type: this.accept
+				})
+				.then(res => {
+					this.pagination = res.pagination;
+					this.list = res.list;
+				})
+				.done(() => {
+					this.loading = false;
+				});
+		},
+
+		// 确认选中
+		confirm() {
+			const urls = this.selection.map(e => e.url).join(",");
+
+			this.$emit("input", urls);
+			this.$emit("confirm", this.detailData ? this.selection : urls);
+
+			this.close();
+		},
+
+		// 选择
+		select(item) {
+			const index = this.selection.findIndex(e => e.id === item.id);
+
+			if (index >= 0) {
+				this.selection.splice(index, 1);
+			} else {
+				if (this.selection.length < this.limit) {
+					this.selection.push(item);
+				}
+			}
+		},
+
+		// 删除选中
+		remove(...selection) {
+			if (isEmpty(selection)) {
+				selection = this.selection;
+			}
+
+			// 已选文件 id
+			const ids = selection.map(e => e.id);
+
+			this.$confirm("此操作将删除文件, 是否继续?", "提示", {
+				type: "warning"
+			})
+				.then(() => {
+					this.$message.success("删除成功");
+
+					// 删除文件及选择
+					ids.forEach(id => {
+						[this.list, this.selection].forEach(list => {
+							const index = list.findIndex(e => e.id === id);
+							list.splice(index, 1);
+						});
+					});
+
+					// 删除请求
+					this.$service.space.info
+						.delete({
+							ids
+						})
+						.catch(err => {
+							this.$message.error(err);
+						});
+				})
+				.catch(() => null);
+		}
+	}
+};
+</script>
+
+<style lang="scss">
+.dialog-upload-space {
+	.el-dialog {
+		&__body {
+			padding: 0;
+		}
+	}
+}
+</style>
+
+<style lang="scss" scoped>
+.cl-upload-space {
+	display: flex;
+	height: 100%;
+	box-sizing: border-box;
+	background-color: #f7f7f7;
+	padding: 5px;
+
+	&__content {
+		flex: 1;
+		max-width: 100%;
+		padding: 0 10px;
+		box-sizing: border-box;
+		background-color: #fff;
+		border-radius: 5px;
+	}
+
+	&__header {
+		display: flex;
+		align-items: center;
+		height: 50px;
+		overflow: auto hidden;
+	}
+
+	&__file {
+		height: calc(100% - 100px);
+		position: relative;
+
+		&-list {
+			display: flex;
+			flex-wrap: wrap;
+		}
+
+		&-empty {
+			display: flex;
+			align-items: center;
+			justify-content: center;
+			position: absolute;
+			top: calc(50% - 90px);
+			left: calc(50% - 160px);
+
+			/deep/.cl-upload {
+				display: flex;
+				flex-direction: column;
+				justify-content: center;
+				align-items: center;
+				border-radius: 6px;
+				cursor: pointer;
+
+				.el-upload-dragger {
+					height: 180px;
+					width: 320px;
+				}
+
+				i {
+					font-size: 67px;
+					color: #c0c4cc;
+				}
+			}
+		}
+	}
+
+	&__footer {
+		padding: 9px 0;
+	}
+}
+</style>