一、文件分享
- 用户可以将自己的文件(单个文件或单个文件夹的方式)以链接 + 提取码的形式分享出去,其它用户(包括分享该文件的用户)都需要先输入提取码,才能够查看用户分享的文件
- 获取分享文件的过程:用户输入用户分享的链接,跳转到文件分享的页面,当前页面会立即发请求给后台,求证当前会话是否该分享输入正确过提取码,如果输入过,则正常获取该分享的文件列表信息。如果没有输入过,那么,让用户跳转到输入提取码的页面,然后,发请求给后台,验证提取码是否正确,如果正确,则跳转到文件分享页面。
- 用户来到文件分享页面,可以预览用户分享的文件、保存用户分享的文件(可单个保存、多个保存)
- 当前用户所分享的文件在查看文件分享页面时,可以取消分享(整个分享全部取消分享)
src/views/main/ShareFile.vue
- Object.assign({},data) 完成赋值
- 复制 分享链接及提取码 (使用vue-clipboard3实现)
- 可以通过
window.location.origin
获取当前的域,可参考:浏览器中location详解
<template>
<div>
<Dialog
:show="dialogConfig.show"
:title="dialogConfig.title"
:buttons="dialogConfig.buttons"
width="600px"
:showCancel="showCancel"
@close="dialogConfig.show = false"
>
<el-form
:model="formData"
:rules="rules"
ref="formDataRef"
label-width="100px"
@submit.prevent
>
<!-- 文件名 -->
<el-form-item label="文件"> {{ formData.fileName }} </el-form-item>
<!-- 选择有效期和提取码 -->
<template v-if="showType == 0">
<!-- 有效期 -->
<el-form-item label="有效期" prop="validType">
<el-radio-group v-model="formData.validType">
<el-radio :label="0">1天</el-radio>
<el-radio :label="1">7天</el-radio>
<el-radio :label="2">30天</el-radio>
<el-radio :label="3">永久有效</el-radio>
</el-radio-group>
</el-form-item>
<!-- 提取码-->
<el-form-item label="提取码" prop="codeType">
<el-radio-group v-model="formData.codeType">
<el-radio :label="0">自定义</el-radio>
<el-radio :label="1">系统生成</el-radio>
</el-radio-group>
</el-form-item>
<!-- 自定义提取码:-->
<el-form-item prop="code" v-if="formData.codeType == 0">
<el-input
clearable
placeholder="请输入5位提取码"
v-model.trim="formData.code"
maxLength="5"
:style="{ width: '130px' }"
></el-input>
</el-form-item>
</template>
<!-- 生成提取码之后,展示分享链接和提取码 -->
<template v-else>
<el-form-item label="分享链接">
{{ shareUrl }}{{ resultInfo.shareId }}
</el-form-item>
<el-form-item label="提取码">
{{ resultInfo.code }}
</el-form-item>
<el-form-item>
<el-button type="primary" @click="copy">复制链接及提取码</el-button>
</el-form-item>
</template>
</el-form>
</Dialog>
</div>
</template>
<script setup>
// 引入实现复制的文件
import useClipboard from "vue-clipboard3";
const { toClipboard } = useClipboard();
import { ref, getCurrentInstance, nextTick } from "vue";
const { proxy } = getCurrentInstance();
// 文档的当前路径
const shareUrl = ref(document.location.origin + "/share/");
const api = {
shareFile: "/share/shareFile",
};
// 辨别是生成分享链接和提取码之前 还是 生成分享之后,生成前为0,生成后为1
// 是否展示分享表单 0:分享表单 1:分享结果
const showType = ref(0);
const formData = ref({});
const formDataRef = ref();
// 表单校验规则
const rules = {
// 必填
validType: [{ required: true, message: "请选择有效期" }],
codeType: [{ required: true, message: "请选择提取码类型" }],
code: [
{ required: true, message: "请输入提取码" },
// Verify.shareCode,之前在Verfiy.js定义过
{ validator: proxy.Verify.shareCode, message: "提取码只能是数字字母" },
{ min: 5, message: "提取码最少5位" },
],
};
// 取消按钮
const showCancel = ref(true);
// 定义弹出框的属性
const dialogConfig = ref({
show: false,
title: "分享",
buttons: [
{
type: "primary",
text: "确定",
click: (e) => {
share();
},
},
],
});
// 结果数据
const resultInfo = ref({});
const share = async () => {
if (Object.keys(resultInfo.value).length > 0) {
dialogConfig.value.show = false;
return;
}
formDataRef.value.validate(async (valid) => {
if (!valid) {
return;
}
let params = {};
Object.assign(params, formData.value);
let result = await proxy.Request({
url: api.shareFile,
params: params,
});
if (!result) {
return;
}
showType.value = 1;
resultInfo.value = result.data;
dialogConfig.value.buttons[0].text = "关闭";
showCancel.value = false;
});
};
// 向父组件 Main 暴露该函数,使得父组件能够调用该函数,并向子组件传递参数
const show = (data) => {
// 触发时,下面的五个值要初始化
showCancel.value = true;
dialogConfig.value.show = true;
dialogConfig.value.buttons[0].text = "确认";
showType.value = 0;
resultInfo.value = {};
nextTick(() => {
// 初始化
formDataRef.value.resetFields();
// 获取数据
formData.value = Object.assign({}, data);
});
};
defineExpose({ show });
// 复制
const copy = async () => {
await toClipboard(
`链接: ${shareUrl.value}${resultInfo.value.shareId} 提取码: ${resultInfo.value.code}`
);
proxy.Message.success("复制成功");
};
</script>
<style lang="scss" scoped>
</style>
Main.vue
<!-- 分享 -->
<ShareFile ref="shareRef"></ShareFile>
import ShareFile from "./ShareFile.vue";
// 分享文件
// 利用ShareFile组件暴露出的show函数,实现将Main组件中的函数传递给ShareFile组件
const shareRef = ref();
const share = (row) => {
shareRef.value.show(row);
};
二、文件已分享列表
src/views/share/Share.vue
- 使用@import引入css,如:
@import "@/assets/file.list.scss";
- vue-clipboard3实现文本内容复制
- 实现批量分享,单个分享,取消分享
<template>
<div>
<!-- 取消分享按钮 -->
<div class="top">
<el-button
type="primary"
:disabled="selectIdList.length == 0"
@click="cancelShareBatch"
>
<span class="iconfont icon-cancel"></span> 取消分享
</el-button>
</div>
<!-- 文件已分享列表 -->
<div class="file-list">
<Table
:columns="columns"
:showPagination="true"
:dataSource="tableData"
:fetch="loadDataList"
:options="tableOptions"
@rowSelected="rowSelected"
>
<!-- 文件名 -->
<template #fileName="{ index, row }">
<!-- showOp(row) 当鼠标放在当前行时,分享下载等图标出现 -->
<!-- cancelShowOp(row) 当鼠标离开当前行时,分享下载等图标消失 -->
<div
class="file-item"
@mouseenter="showOp(row)"
@mouseleave="cancelShowOp(row)"
>
<!-- 显示文件图标:不同类型 -->
<template v-if="
(row.fileType == 3 || row.fileType == 1) && row.status !== 0
"
>
<!-- 如果文件类型是图片或者视频,且已经成功转码,则执行 Icon中的cover -->
<Icon :cover="row.fileCover"></Icon>
</template>
<template v-else>
<!-- 如果文件夹类型是文件,则文件类型是该文件类型 -->
<Icon v-if="row.folderType == 0" :fileType="row.fileType"></Icon>
<!-- 如果文件夹类型是目录,则文件类型就是目录0 -->
<Icon v-if="row.folderType == 1" :fileType="0"></Icon>
</template>
<!-- 显示文件名称 -->
<span
class="file-name"
v-if="!row.showRename"
:title="row.fileName"
>
<span>{{ row.fileName }}</span>
</span>
<!-- 当鼠标放在当前行时显示 -->
<span class="op">
<template v-if="row.showOp && row.fileId">
<span class="iconfont icon-link" @click="copy(row)">
复制链接
</span>
<span class="iconfont icon-cancel" @click="cancelShare(row)">
取消分享
</span>
</template>
</span>
</div>
</template>
<template #expireTime="{ index, row }">
{{ row.validType == 3 ? "永久" : row.expireTime }}
</template>
</Table>
</div>
</div>
</template>
<script setup>
// 引入实现复制的文件
import useClipboard from "vue-clipboard3";
const { toClipboard } = useClipboard();
import { ref, reactive, getCurrentInstance, watch } from "vue";
const { proxy } = getCurrentInstance();
const api = {
loadDataList: "/share/loadShareList",
cancelShare: "/share/cancelShare",
};
// 列表头信息
const columns = [
{
label: "文件名",
prop: "fileName",
scopedSlots: "fileName",
},
{
label: "分享时间",
prop: "shareTime",
width: 200,
},
{
label: "失效时间",
prop: "expireTime",
scopedSlots: "expireTime",
width: 200,
},
{
label: "浏览次数",
prop: "showCount",
width: 200,
},
];
// 数据源
const tableData = ref({});
// 表格选项
const tableOptions = {
extHeight: 20,
selectType: "checkbox",
};
// 获得数据;
const loadDataList = async () => {
let params = {
// 页码
pageNo: tableData.value.pageNo,
// 分页大小
pageSize: tableData.value.pageSize,
};
if (params.category !== "all") {
delete params.filePid;
}
let result = await proxy.Request({
url: api.loadDataList,
params,
});
if (!result) {
return;
}
tableData.value = result.data;
};
// 当鼠标放在当前行时,分享下载等图标出现
// 展示操作
const showOp = (row) => {
// 关闭所有的显示
tableData.value.list.forEach((element) => {
element.showOp = false;
});
// 只开启当前显示
row.showOp = true;
};
// 取消展示
const cancelShowOp = (row) => {
row.showOp = false;
};
// 文档的当前路径
const shareUrl = ref(document.location.origin + "/share/");
// 复制
const copy = async (data) => {
await toClipboard(
`链接: ${shareUrl.value}${data.shareId} 提取码: ${data.code}`
);
proxy.Message.success("复制成功");
};
// 行选中
// 多选 批量选中
const selectIdList = ref([]);
const rowSelected = (rows) => {
selectIdList.value = [];
rows.forEach((item) => {
selectIdList.value.push(item.shareId);
});
};
const cancelShareIdList = ref([]);
// 批量取消分享
const cancelShareBatch = () => {
if (selectIdList.value.length == 0) {
return;
}
cancelShareIdList.value = selectIdList.value;
cancelShareDone();
};
// 单个取消分享
const cancelShare = (row) => {
cancelShareIdList.value = [row.shareId];
cancelShareDone();
};
// 取消分享http请求
const cancelShareDone = async () => {
proxy.Confirm(`你确定要取消分享吗?`, async () => {
let result = await proxy.Request({
url: api.cancelShare,
params: {
shareIds: cancelShareIdList.value.join(","),
},
});
if (!result) {
return;
}
proxy.Message.success("取消分享成功");
loadDataList();
});
};
</script>
<style lang="scss" scoped>
@import "@/assets/file.list.scss";
.file-list {
margin-top: 10px;
.file-item {
.file-name {
span {
&:hover {
color: #494944;
}
}
}
.op {
width: 170px;
}
}
}
</style>
三、外部分享界面
1.添加路由
src/router/index.js
在framework之外添加:
{
path: '/shareCheck/:shareId',
name: '分享校验',
component: () =>
import ("@/views/webshare/ShareCheck.vue")
},
{
path: '/share/:shareId',
name: '分享',
component: () =>
import ("@/views/webshare/Share.vue")
},
2.用户输入链接,进入验证界面
其它用户拿到链接之后,将分享链接输入到地址栏,但是,用户第一次输入的话,肯定没有验证过(会话中,没有此分享记录),因此,要跳转到分享验证页面,验证通过之后,该分享记录会存入此会话,当用户再一次进行此分享页面,就会从会话中检测到有此分享记录,此时就不需要再次验证了。
src/views/webshare/ShareCheck.vue
- 背景图片设置,可参考:CSS背景background设置
<template>
<div class="share">
<div class="body-content">
<!-- logo+标题 -->
<div class="logo">
<span class="iconfont icon-pan"></span>
<span class="name">uu云盘</span>
</div>
<div class="code-panel">
<!-- 个人信息,头像+名字 -->
<div class="file-info">
<!-- 头像 -->
<div class="avatar">
<Avatar :userId="shareInfo.userId" :avatar="shareInfo.avatar" :width="50"></Avatar>
</div>
<!-- 分享信息 -->
<div class="share-info">
<div class="user-info">
<!-- 名字 -->
<span class="nick-name">{{ shareInfo.nickName }} </span>
<span class="share-time">分享于 {{ shareInfo.shareTime }}</span>
</div>
<div class="file-name">分享文件:{{ shareInfo.fileName }}</div>
</div>
</div>
<!-- 提取码+按钮 -->
<div class="code-body">
<div class="tips">请输入提取码:</div>
<div class="input-area">
<el-form :model="formData" :rules="rules" ref="formDataRef" :maxLength="5" @submit.prevent>
<el-form-item prop="code">
<el-input class="input" v-model="formData.code" @keyup.enter="checkShare"></el-input>
<el-button type="primary" @click="checkShare">提取文件</el-button>
</el-form-item>
</el-form>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, getCurrentInstance, nextTick } from "vue";
import { useRouter, useRoute } from "vue-router";
const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();
const api = {
getShareInfo: "/showShare/getShareInfo",
checkShareCode: "/showShare/checkShareCode",
};
const shareId = route.params.shareId;
const shareInfo = ref({});
// 获取数据
const getShareInfo = async () => {
let result = await proxy.Request({
url: api.getShareInfo,
params: {
shareId,
},
});
if (!result) {
return;
}
shareInfo.value = result.data;
};
getShareInfo();
const formData = ref({});
const formDataRef = ref();
// 校验规则
const rules = {
code: [
{ required: true, message: "请输入提取码" },
{ min: 5, message: "提取码为5位" },
{ max: 5, message: "提取码为5位" },
],
};
// 验证成功,推到share页面
const checkShare = async () => {
formDataRef.value.validate(async (valid) => {
if (!valid) {
return;
}
let result = await proxy.Request({
url: api.checkShareCode,
params: {
shareId: shareId,
code: formData.value.code,
},
});
if (!result) {
return;
}
router.push(`/share/${shareId}`);
});
};
</script>
<style lang="scss" scoped>
.share {
height: calc(100vh);
background: url("../../assets/share_bg.png");
background-repeat: repeat-x;
background-position: 0 bottom;
background-color: #eef2f6;
display: flex;
justify-content: center;
.body-content {
margin-top: calc(100vh / 5);
width: 500px;
.logo {
display: flex;
align-items: center;
justify-content: center;
.icon-pan {
font-size: 60px;
color: #409eff;
}
.name {
font-weight: bold;
margin-left: 5px;
font-size: 25px;
color: #409eff;
}
}
.code-panel {
margin-top: 20px;
background: #fff;
border-radius: 5px;
overflow: hidden;
box-shadow: 0 0 7px 1px #5757574f;
.file-info {
padding: 10px 20px;
background: #409eff;
color: #fff;
display: flex;
align-items: center;
.avatar {
margin-right: 5px;
}
.share-info {
.user-info {
display: flex;
align-items: center;
.nick-name {
font-size: 15px;
}
.share-time {
margin-left: 20px;
font-size: 12px;
}
}
.file-name {
margin-top: 10px;
font-size: 12px;
}
}
}
.code-body {
padding: 30px 20px 60px 20px;
.tips {
font-weight: bold;
}
.input-area {
margin-top: 10px;
.input {
flex: 1;
margin-right: 10px;
}
}
}
}
}
}
</style>
3.验证成功后,进入分享界面
- 进入分享页面,首先仍然要请求接口,判断当前会话是否已经输入当前分享的提取码,如果没有输入,则跳转到验证页面
src/views/webshare/Share.vue
<template>
<div class="share">
<!-- 头部 -->
<div class="header">
<div class="header-content">
<div class="logo" @click="jump">
<span class="iconfont icon-pan"></span>
<span class="name">uu云盘</span>
</div>
</div>
</div>
<div class="share-body">
<!-- 判断 -->
<template v-if="Object.keys(shareInfo).length == 0">
<div
v-loading="Object.keys(shareInfo).length == 0"
class="loading"
></div>
</template>
<template v-else>
<!-- 分享用户信息时间等 -->
<div class="share-panel">
<div class="share-user-info">
<div class="avatar">
<Avatar
:userId="shareInfo.userId"
:avatar="shareInfo.avatar"
:width="50"
></Avatar>
</div>
<div class="share-info">
<div class="user-info">
<span class="nick-name">{{ shareInfo.nickName }} </span>
<span class="share-time">分享于 {{ shareInfo.shareTime }}</span>
</div>
<div class="file-name">分享文件:{{ shareInfo.fileName }}</div>
</div>
</div>
<div class="share-op-btn">
<el-button
type="primary"
v-if="shareInfo.currentUser"
@click="cancelShare"
><span class="iconfont icon-cancel"></span>取消分享</el-button
>
<el-button
type="primary"
v-else
:disabled="selectFileIdList.length == 0"
@click="save2MyPan"
><span class="iconfont icon-import"></span
>保存到我的网盘</el-button
>
</div>
</div>
<!-- 导航栏 -->
<Navigation
ref="navigationRef"
@navChange="navChange"
:shareId="shareId"
></Navigation>
<!-- 分享文件列表 -->
<div class="file-list">
<Table
:columns="columns"
:showPagination="true"
:dataSource="tableData"
:fetch="loadDataList"
:initFetch="false"
:options="tableOptions"
@rowSelected="rowSelected"
>
<!-- 文件名 -->
<template #fileName="{ index, row }">
<!-- showOp(row) 当鼠标放在当前行时,分享下载等图标出现 -->
<!-- cancelShowOp(row) 当鼠标离开当前行时,分享下载等图标消失 -->
<div
class="file-item"
@mouseenter="showOp(row)"
@mouseleave="cancelShowOp(row)"
>
<!-- 显示文件图标 -->
<template
v-if="
(row.fileType == 3 || row.fileType == 1) && row.status !== 0
"
>
<!-- 如果文件类型是图片或者视频,且已经成功转码,则执行 Icon中的cover -->
<Icon :cover="row.fileCover"></Icon>
</template>
<template v-else>
<!-- 如果文件夹类型是文件,则文件类型是该文件类型 -->
<Icon
v-if="row.folderType == 0"
:fileType="row.fileType"
></Icon>
<!-- 如果文件夹类型是目录,则文件类型就是目录0 -->
<Icon v-if="row.folderType == 1" :fileType="0"></Icon>
</template>
<!-- 显示文件名称 -->
<!-- v-if="!row.showEdit" 如果该行文件没有编辑 -->
<span class="file-name" :title="row.fileName">
<span @click="preview(row)">{{ row.fileName }}</span>
</span>
<!-- 当鼠标放在当前行时显示 -->
<span class="op">
<!-- 只有当是文件夹时才可下载 -->
<span
v-if="row.folderType == 0"
class="iconfont icon-download"
@click="download(row.fileId)"
>下载</span
>
<template v-if="row.showOp && !shareInfo.currentUser">
<span
class="iconfont icon-import"
@click="save2MyPanSingle(row)"
>保存到我的网盘</span
>
</template>
</span>
</div>
</template>
<!-- 文件大小 -->
<template #fileSize="{ index, row }">
<span v-if="row.fileSize">
{{ proxy.Utils.size2Str(row.fileSize) }}</span
>
</template>
</Table>
</div>
</template>
<!-- 选择目录 -->
<FolderSelect
ref="folderSelectRef"
@folderSelect="save2MyPanDone"
></FolderSelect>
<!-- 预览 -->
<Preview ref="previewRef"></Preview>
</div>
</div>
</template>
<script setup>
import { ref, reactive, getCurrentInstance, watch } from "vue";
import { useRouter, useRoute } from "vue-router";
const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();
const api = {
getShareLoginInfo: "/showShare/getShareLoginInfo",
loadFileList: "/showShare/loadFileList",
createDownloadUrl: "/showShare/createDownloadUrl",
download: "/api/showShare/download",
cancelShare: "/share/cancelShare",
saveShare: "/showShare/saveShare",
};
const shareId = route.params.shareId;
const shareInfo = ref({});
// 获取数据
const getShareInfo = async () => {
let result = await proxy.Request({
url: api.getShareLoginInfo,
params: {
shareId,
},
});
if (result.data == null) {
router.push("/shareCheck/" + shareId);
return;
}
shareInfo.value = result.data;
};
getShareInfo();
//列表
const columns = [
{
label: "文件名",
prop: "fileName",
scopedSlots: "fileName",
},
{
label: "修改时间",
prop: "lastUpdateTime",
width: 200,
},
{
label: "大小",
prop: "fileSize",
scopedSlots: "fileSize",
width: 200,
},
];
const tableData = ref({});
const tableOptions = {
extHeight: 80,
selectType: "checkbox",
};
// 获得数据;
const loadDataList = async () => {
let params = {
// 页码
pageNo: tableData.value.pageNo,
// 分页大小
pageSize: tableData.value.pageSize,
shareId: shareId,
filePid: currentFolder.value.fileId,
};
let result = await proxy.Request({
url: api.loadFileList,
params,
});
if (!result) {
return;
}
tableData.value = result.data;
};
// 当鼠标放在当前行时,分享下载等图标出现
const showOp = (row) => {
// 关闭所有的显示
tableData.value.list.forEach((element) => {
element.showOp = false;
});
// 只开启当前显示
row.showOp = true;
};
const cancelShowOp = (row) => {
row.showOp = false;
};
// 行选中
// 多选 批量选中
const selectFileIdList = ref([]);
const rowSelected = (rows) => {
selectFileIdList.value = [];
rows.forEach((item) => {
selectFileIdList.value.push(item.fileId);
});
};
const currentFolder = ref({ fileId: "0" });
// 目录
const navChange = (data) => {
const { curFolder } = data;
currentFolder.value = curFolder;
loadDataList();
};
// 预览
const previewRef = ref();
const preview = (data) => {
// 如果是目录(文件夹)
if (data.folderType == 1) {
navigationRef.value.openFolder(data);
return;
}
data.shareId = shareId;
previewRef.value.showPreview(data, 2);
};
// 下载文件
const download = async (fileId) => {
let result = await proxy.Request({
url: api.createDownloadUrl + "/" + shareId + "/" + fileId,
});
if (!result) {
return;
}
window.location.href = api.download + "/" + result.data;
};
// 保存到我的网盘
const folderSelectRef = ref();
const save2MyPanFileIdArray = [];
// 批量保存
const save2MyPan = () => {
if (selectFileIdList.value.length == 0) {
return;
}
if (!proxy.VueCookies.get("userInfo")) {
router.push("/login?redirectUrl=" + route.path);
return;
}
save2MyPanFileIdArray.values = selectFileIdList.value;
folderSelectRef.value.showFolderDialog();
};
// 单个保存
const save2MyPanSingle = (row) => {
if (!proxy.VueCookies.get("userInfo")) {
router.push("/login?redirectUrl=" + route.path);
return;
}
save2MyPanFileIdArray.values = [row.fileId];
folderSelectRef.value.showFolderDialog();
};
// 执行保存操作
const save2MyPanDone = async (folderId) => {
let result = await proxy.Request({
url: api.saveShare,
params: {
shareId: shareId,
shareFileIds: save2MyPanFileIdArray.values.join(","),
myFolderId: folderId,
},
});
if (!result) {
return;
}
loadDataList();
proxy.Message.success("保存成功");
folderSelectRef.value.close();
};
const cancelShare = () => {
proxy.Confirm(`你确定要取消分享吗?`, async () => {
let result = await proxy.Request({
url: api.cancelShare,
params: {
shareIds: shareId,
},
});
if (!result) {
return;
}
proxy.Message.success("取消分享成功");
router.push("/");
});
};
const jump = () => {
router.push("/");
};
</script>
<style lang="scss" scoped>
@import "@/assets/file.list.scss";
.header {
width: 100%;
position: fixed;
background: #0c95f7;
height: 50px;
.header-content {
width: 70%;
margin: 0px auto;
color: #fff;
line-height: 50px;
.logo {
display: flex;
align-items: center;
cursor: pointer;
.icon-pan {
font-size: 40px;
}
.name {
font-weight: bold;
margin-left: 5px;
font-size: 25px;
}
}
}
}
.share-body {
width: 70%;
margin: 0px auto;
padding-top: 50px;
.loading {
height: calc(100vh / 2);
width: 100%;
}
.share-panel {
margin-top: 20px;
display: flex;
justify-content: space-around;
border-bottom: 1px solid #ddd;
padding-bottom: 10px;
.share-user-info {
flex: 1;
display: flex;
align-items: center;
.avatar {
margin-right: 5px;
}
.share-info {
.user-info {
display: flex;
align-items: center;
.nick-name {
font-size: 15px;
}
.share-time {
margin-left: 20px;
font-size: 12px;
}
}
.file-name {
margin-top: 10px;
font-size: 12px;
}
}
}
}
}
.file-list {
margin-top: 10px;
.file-item {
.op {
width: 170px;
}
}
}
</style>