Parcourir la source

新增租户账号、租户平台logo、平台名称、所属区域等信息

pull/1/head
余菲 il y a 2 ans
Parent
révision
af24746ee3
6 fichiers modifiés avec 721 ajouts et 208 suppressions
  1. +1
    -0
      package.json
  2. +61
    -0
      src/api/upload.js
  3. +274
    -0
      src/components/UploadOss.vue
  4. +81
    -0
      src/utils/request.js
  5. +28
    -62
      src/views/data/tenant/index.vue
  6. +276
    -146
      src/views/data/tenant/tenant-edit.vue

+ 1
- 0
package.json Voir le fichier

@@ -15,6 +15,7 @@
"dependencies": {
"@amap/amap-jsapi-loader": "~1.0.1",
"@tinymce/tinymce-vue": "~4.0.4",
"ali-oss": "^6.17.1",
"ant-design-vue": "~2.2.2",
"axios": "~0.21.1",
"core-js": "^3.25.0",

+ 61
- 0
src/api/upload.js Voir le fichier

@@ -0,0 +1,61 @@
import request from '@/utils/request.js'

/**
* @description: 获取音视频上传地址和凭证
* @param {Object} params
* @return {Object}
*/
export function getAuth(params) {
return request({
url: '/aliyuncsVod/createUploadVideo',
method: 'get',
responseType: 'json',
params
})
}

/**
* @description: 刷新音/视频上传凭证
* @param {String} videoId
* @return {*}
*/
export function refreshAuth(videoId) {
return request({
url: '/aliyuncsVod/refreshUploadVideo',
method: 'get',
responseType: 'json',
params: {
videoId
}
})
}

/**
* @description: 获取图片上传鉴权
* @param {String} objectName
* @return {*}
*/
export function getOssAuth(objectName) {
return request({
url: '/aliyunOss/getSecurityToken',
method: 'get',
responseType: 'json',
params: {
objectName
}
})
}

/**
* @description: 上传离线视频地址
* @param {String} params
* @return {*}
*/
export function uploadVideoUrl(data) {
return request({
url: '/inspection/uploadVideoUrl',
method: 'post',
data
})
}


+ 274
- 0
src/components/UploadOss.vue Voir le fichier

@@ -0,0 +1,274 @@
<template>
<div>
<a-upload
name="avatar"
list-type="picture-card"
:file-list="fileList"
:show-upload-list="{showPreviewIcon:false,showRemoveIcon:true}"
class="avatar-uploader"
action="#"
:before-upload="beforeUpload"
:remove="handleRemove"
@preview="handlePreview"
@change="handleChange"
>
<div v-if="fileList.length < limit">
<div class="ant-upload-text" />
</div>
</a-upload>

<a-modal :visible="previewVisible" :footer="null" @cancel="handleCancel">
<img alt="example" style="width: 100%" :src="previewImage">
</a-modal>
</div>
</template>

<script>
import OSS from 'ali-oss'
import { getOssAuth } from '@/api/upload.js'
import { message } from 'ant-design-vue'
import { defineComponent, reactive, onMounted, watch, toRefs } from 'vue'
export default defineComponent({
name: 'UploadOss',
props: {
limit: {
type: Number,
default: 1
},
defaultList: {
type: String,
default: ''
}
},
emits: ['uploadSuccess'],
setup(props, { emit }) {
const data = reactive({
...props,
client: null,
fileList: [],
uploadFile: [],
previewImage: '',
previewVisible: false
})
onMounted(() => {
if (props.defaultList) {
const list = props.defaultList.split(',')
const fileList = list.map((item) => {
const objectName = item.split('/').slice(3).join('/')
const obj = {
name: objectName,
uid: objectName,
type: 'image/png',
url: item,
objectName: objectName
}
return obj
})
data.fileList = fileList
emit('uploadSuccess', 'upload')
} else {
data.fileList = []
}
data.uploadFile = []
data.previewImage = ''
data.previewVisible = false
})

watch(() => props.defaultList, (newValue, oldValue) => {
if (newValue.length) {
const list = newValue.split(',')
const fileList = list.map((item) => {
const objectName = item.split('/').slice(3).join('/')
const obj = {
name: objectName,
uid: objectName,
type: 'image/png',
url: item,
objectName: objectName
}
return obj
})
data.fileList = fileList
emit('uploadSuccess', 'upload')
} else {
data.fileList = []
}
})

function beforeUpload(file) {
if (!file.type.match('image.*')) {
message.error('请选择正确的图片类型')
} else {
data.uploadFile.push(file)
// const client = new OSS({
// region: 'oss-cn-shanghai',
// secure: true,
// accessKeyId: 'LTAI5tSJ62TLMUb4SZuf285A',
// accessKeySecret: 'MWYynm30filZ7x0HqSHlU3pdLVNeI7',
// bucket: 'ta-tech-image'
// })
// client.put(`imagedir/${file.name}`, file)
// .then((res) => {
// const imageUrl = client.signatureUrl(`imagedir/${file.name}`)
// const length = data.fileList.length
// data.fileList[length - 1].url = imageUrl
// emit('uploadSuccess', data.fileList)
// })
// .catch((err) => {
// const length = data.fileList.length
// data.fileList.splice(length - 1, 1)
// message.error('上传图片失败,请重新选择上传!')
// console.log(err)
// })
}
/* 中断a-upload的文件上传 */
return false
}

function startUpload() {
return new Promise((resolve, reject) => {
const uploads = []
const uloadList = data.uploadFile.filter((item) => {
const index = data.fileList.findIndex(list => list.uid === item.uid)
const result = index === -1 ? false : !data.fileList[index].url
return result
})
if (!uloadList.length) {
const objectList = data.fileList.map((item) => item.objectName || 'error')
resolve(objectList)
} else {
getOssAuth()
.then(res => {
data.client = new OSS({
region: 'oss-cn-shanghai',
secure: true,
accessKeyId: res.accessKeyId,
accessKeySecret: res.accessKeySecret,
stsToken: res.securityToken,
bucket: 'ta-tech-image'
})
uloadList.forEach(item => {
uploads.push(uploadOss(item))
})
Promise.all(uploads)
.then((res) => {
const objectList = data.fileList.map((item) => item.objectName || 'error')
resolve(objectList)
})
.catch((error) => {
resolve(error)
})
})
}
})
}

function uploadOss(file) {
return new Promise((resolve, reject) => {
const randomString = Math.random().toString(36).slice(2)
const suffix = /\.[^\.]+/.exec(file.name)
const timestamp = new Date().getTime()
const objectName = `imagedir/${randomString}_${timestamp}${suffix}`
data.client.put(objectName, file)
.then((res) => {
const index = data.fileList.findIndex(list => list.uid === file.uid)
data.fileList[index].url = res.url
data.fileList[index].objectName = objectName
resolve(objectName)
})
.catch((err) => {
resolve('error')
console.log(err)
})
})
}

/**
* @description: 文件发生变化时
* @param {Array} fileList
* @return {*}
*/
function handleChange({ fileList }) {
data.fileList = fileList.filter((item) => {
return item.type.match('image.*')
})
const status = data.fileList.length ? 'ready' : ''
emit('uploadSuccess', status)
}

/**
* @description: 文件删除时的回调
* @param {Object} file
* @return {*}
*/
function handleRemove(file) {
const index = data.fileList.findIndex((item) => {
return item.uid === file.uid
})
data.uploadFile.splice(index, 1)
}

/**
* @description: 关闭预览图
* @param {Object} file
* @return {*}
*/
function handlePreview(file) {
data.previewImage = file.url || file.thumbUrl
data.previewVisible = true
}

/**
* @description: 关闭图片预览框
*/
function handleCancel() {
data.previewVisible = false
}

return {
...toRefs(data),
beforeUpload,
handlePreview,
handleChange,
handleRemove,
handleCancel,
startUpload,
uploadOss
}
}
})
</script>

<style lang="less" scoped>
.ant-upload-picture-card-wrapper{
min-height: 122px;
padding: 10px 0 0 10px;
}
.ant-upload-text {
// width: 100px;
// height: 100px;
// cursor: pointer;
// border: 1px solid #e1e1e1;
// margin: 10px 0 0 10px;
position: relative;
}
.ant-upload-text::before {
content: '';
position: absolute;
left: 50%;
top: 50%;
width: 30px;
transform:translate(-50%, -50%);
border-top: 2px solid #e1e1e1;
}

.ant-upload-text::after {
content: '';
position: absolute;
left: 50%;
top: 50%;
height: 30px;
transform: translate(-50%,-50%);
border-left: 2px solid #e1e1e1;
}
</style>

+ 81
- 0
src/utils/request.js Voir le fichier

@@ -0,0 +1,81 @@
import axios from 'axios'
import store from '@/store'
import router from '@/router'
import setting from '@/config/setting'
import { Modal } from 'ant-design-vue'

// create an axios instance
const service = axios.create({
baseURL: '/api', // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
timeout: process.env.NODE_ENV === 'development' ? 0 : 600000 // request timeout
})

// request interceptor
service.interceptors.request.use(
config => {
const token = setting.takeToken()
if (token) {
config.headers[setting.tokenHeaderName] = token
}
// do something before request is sent
return config
},
error => {
// do something with request error
console.log(error) // for debug
return Promise.reject(error)
}
)

// response interceptor
service.interceptors.response.use(
async response => {
// 登录过期处理
if (response.data.code === 401) {
if (response.config.url === setting.menuUrl) {
goLogin()
} else {
Modal.destroyAll()
Modal.info({
title: '系统提示',
content: '登录状态已过期, 请退出重新登录!',
okText: '重新登录',
onOk: () => {
goLogin(true)
}
})
}
return Promise.reject(new Error(response.data.msg))
}
// token自动续期
const access_token = response.headers[setting.tokenHeaderName]
if (access_token) {
setting.cacheToken(access_token)
}
return response.data
},
error => {
console.log('err' + error) // for debug
return Promise.reject(error)
}
)

/**
* 跳转到登录页面
*/
function goLogin(reload) {
store.dispatch('user/removeToken').then(() => {
if (reload) {
location.replace('/login') // 这样跳转避免再次登录重复注册动态路由
} else {
const path = router.currentRoute.path
return router.push({
path: '/login',
query: path && path !== '/' ? { form: path } : null
})
}
})
}

export default service

+ 28
- 62
src/views/data/tenant/index.vue Voir le fichier

@@ -52,7 +52,11 @@
</template>
<!-- 租户头像 -->
<template #logo="{ record }">
<a-image :width="35" :src="record.logo"/>
<a-image :width="35" :height="35" :src="record.logo"/>
</template>
<!-- 所属区域 -->
<template #provinceName="{record}">
<span>{{record.provinceName + record.cityName + record.districtName + ''}}</span>
</template>
<!-- 租户类型 -->
<template #type="{ record }">
@@ -71,12 +75,6 @@
<template #action="{ record }">
<a-space>
<a @click="openEdit(record)">修改</a>
<!-- <a-divider type="vertical"/>
<a-popconfirm
title="确定要删除此租户吗?"
@confirm="remove(record)">
<a class="ele-text-danger">删除</a>
</a-popconfirm> -->
</a-space>
</template>
</ele-pro-table>
@@ -108,73 +106,41 @@ export default {
// 表格列配置
columns: [
{
title: '号',
dataIndex: 'id',
title: '租户编号',
dataIndex: 'code',
align: 'center',
width: 160,
width: 120,
},
{
title: '租户名称',
dataIndex: 'name',
align: 'center',
width: 200,
width: 120,
},
{
title: '租户账号',
dataIndex: 'username',
align: 'code',
align: 'center',
width: 150,
},
// {
// title: '租户头像',
// dataIndex: 'logo',
// align: 'center',
// slots: {customRender: 'logo'}
// },
// {
// title: '租户类型',
// dataIndex: 'type',
// align: 'center',
// width: 100,
// slots: {customRender: 'type'}
// },
// {
// title: '租户电话',
// dataIndex: 'phone',
// align: 'center',
// width: 150,
// },
// {
// title: '租户邮箱',
// dataIndex: 'email',
// align: 'center',
// width: 150,
// },
// {
// title: '租户地址',
// dataIndex: 'address',
// align: 'center',
// width: 250,
// },
// {
// title: '租户状态',
// dataIndex: 'status',
// width: 100,
// align: 'center',
// slots: {customRender: 'status'}
// },
// {
// title: '排序',
// dataIndex: 'sort',
// width: 100,
// align: 'center'
// },
// {
// title: '备注',
// dataIndex: 'note',
// width: 200,
// align: 'center'
// },
{
title: '平台LOGO',
dataIndex: 'logo',
align: 'center',
slots: { customRender: 'logo' }
},
{
title: '平台名称',
dataIndex: 'platformName',
align: 'center',
width: 150,
},
{
title: '所属区域',
dataIndex: 'provinceName',
align: 'center',
slots: { customRender: 'provinceName' }
},
{
title: '创建时间',
dataIndex: 'createTime',

+ 276
- 146
src/views/data/tenant/tenant-edit.vue Voir le fichier

@@ -1,129 +1,170 @@
<!-- 租户编辑弹窗 -->
<template>
<a-modal
:width="500"
:width="640"
:visible="visible"
:confirm-loading="loading"
:title="isUpdate?'修改租户':'添加租户'"
:body-style="{paddingBottom: '8px'}"
:title="isUpdate ? '修改租户' : '添加租户'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
@ok="save">
@ok="save"
>
<a-form
ref="form"
:model="form"
:rules="rules"
:label-col="{md: {span: 6}, sm: {span: 24}}"
:wrapper-col="{md: {span: 19}, sm: {span: 24}}">
<!-- <a-form-item
label="租户头像:"
name="logo"
:label-col="{sm: {span: 3}, xs: {span: 6}}"
:wrapper-col="{sm: {span: 21}, xs: {span: 18}}">
<uploadImage :limit="1" :updDir="updDir" v-model:value="form.logo"/>
</a-form-item> -->
<a-form-item label="租户名称:" name="name">
<a-input
allow-clear
:maxlength="150"
placeholder="请输入租户名称"
v-model:value="form.name"/>
</a-form-item>
<!-- <a-form-item label="租户电话:" name="phone">
<a-input
allow-clear
:maxlength="50"
placeholder="请输入租户电话"
v-model:value="form.phone"/>
</a-form-item> -->
<!-- <a-form-item label="租户地址:" name="address">
<a-input
allow-clear
:maxlength="150"
placeholder="请输入租户地址"
v-model:value="form.address"/>
</a-form-item> -->
<!-- <a-form-item label="排序号:" name="sort">
<a-input-number
:min="0"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sort"/>
</a-form-item> -->
<a-form-item label="用户账号:" name="username">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入用户账号"
v-model:value="form.username"/>
</a-form-item>
<!-- <a-form-item label="租户编码:" name="code">
<a-input
allow-clear
:maxlength="30"
placeholder="请输入租户编码"
v-model:value="form.code"/>
</a-form-item> -->
<!-- <a-form-item label="租户邮箱:" name="email">
<a-input
allow-clear
:maxlength="50"
placeholder="请输入租户邮箱"
v-model:value="form.email"/>
</a-form-item> -->
<!-- <a-form-item
label="租户类型:"
name="type">
<a-select
v-model:value="form.type"
placeholder="请选择租户类型"
allow-clear>
<a-select-option :value="1">政府</a-select-option>
<a-select-option :value="2">企业</a-select-option>
<a-select-option :value="3">组织</a-select-option>
</a-select>
</a-form-item> -->
<!-- <a-form-item label="租户状态" name="status">
<a-radio-group
v-model:value="form.status">
<a-radio :value="1">已启用</a-radio>
<a-radio :value="2">未启用</a-radio>
</a-radio-group>
</a-form-item> -->
<a-form-item
label="登录密码:"
name="password">
<a-input-password
:maxlength="20"
placeholder="请输入登录密码"
v-model:value="form.password"/>
</a-form-item>
<!-- <a-form-item
label="备注:"
:label-col="{sm: {span: 3}, xs: {span: 6}}"
:wrapper-col="{sm: {span: 21}, xs: {span: 18}}">
<a-textarea
:rows="3"
:maxlength="255"
placeholder="请输入备注"
v-model:value="form.note"/>
</a-form-item> -->
:label-col="{ md: { span: 4 }, sm: { span: 24 } }"
:wrapper-col="{ md: { span: 20 }, sm: { span: 24 } }"
>
<a-row :gutter="16">
<a-col :md="24" :sm="24" :xs="24">
<a-form-item label="平台logo:">
<UploadOss v-if="visible" ref="logoUpload" :default-list="initUpload"/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :md="24" :sm="24" :xs="24">
<a-form-item label="租户名称:" name="name">
<a-input
allow-clear
:maxlength="150"
placeholder="请输入租户名称"
v-model:value="form.name"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :md="24" :sm="24" :xs="24">
<a-form-item label="用户账号:" name="username">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入用户账号"
v-model:value="form.username"
/>
</a-form-item>
</a-col>
</a-row>
<a-row v-if="!isUpdate" :gutter="16">
<a-col :md="24" :sm="24" :xs="24">
<a-form-item label="登录密码:" name="password">
<a-input-password
:maxlength="20"
placeholder="请输入登录密码"
v-model:value="form.password"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :md="10" :sm="8" :xs="24">
<a-form-item
label="所属区域:"
name="provinceCode"
:label-col="{ sm: { span: 10 }, xs: { span: 9 } }"
>
<a-select
v-model:value="form.provinceCode"
:get-popup-container="
(triggerNode) => {
return triggerNode.parentNode;
}
"
style="width: 100%"
placeholder="请选择省"
@change="selectArea(form.provinceCode, 'province', 'city')"
>
<a-select-option
v-for="(item, index) in provinceList"
:key="index"
:value="item.citycode"
>{{ item.name }}</a-select-option
>
</a-select>
</a-form-item>
</a-col>
<a-col :md="7" :sm="4" :xs="24">
<a-form-item
name="cityCode"
:wrapper-col="{ sm: { span: 24 }, xs: { span: 24 } }"
>
<a-select
v-model:value="form.cityCode"
:get-popup-container="
(triggerNode) => {
return triggerNode.parentNode;
}
"
style="width: 100%"
placeholder="请选择市"
@change="selectArea(form.cityCode, 'city', 'district')"
>
<a-select-option
v-for="(item, index) in cityList"
:key="index"
:value="item.citycode"
>{{ item.name }}</a-select-option
>
</a-select>
</a-form-item>
</a-col>
<a-col :md="7" :sm="4" :xs="24">
<a-form-item
:wrapper-col="{ sm: { span: 24 }, xs: { span: 24 } }"
name="districtCode"
>
<a-select
v-model:value="form.districtCode"
:get-popup-container="
(triggerNode) => {
return triggerNode.parentNode;
}
"
style="width: 100%"
placeholder="请选择区"
@change="selectArea(form.districtCode, 'district', 'street')"
>
<a-select-option
v-for="(item, index) in districtList"
:key="index"
:value="item.citycode"
>{{ item.name }}</a-select-option
>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :md="24" :sm="24" :xs="24">
<a-form-item label="平台名称:" name="platformName">
<a-input
allow-clear
:maxlength="150"
placeholder="请输入平台名称"
v-model:value="form.platformName"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-modal>
</template>

<script>
import uploadImage from '@/components/uploadImage'
import UploadOss from "@/components/UploadOss.vue";

export default {
name: 'TenantEdit',
emits: ['done', 'update:visible'],
name: "TenantEdit",
emits: ["done", "update:visible"],
props: {
// 弹窗是否打开
visible: Boolean,
// 修改回显的数据
data: Object
data: Object,
},
components: {uploadImage},
components: { UploadOss },
data() {
return {
// 表单数据
@@ -131,79 +172,168 @@ export default {
// 表单验证规则
rules: {
name: [
{required: true, message: '请输入租户名称', type: 'string', trigger: 'blur'}
{
required: true,
message: "请输入租户名称",
type: "string",
trigger: "blur",
},
],
provinceCode: [
{
required: true,
message: "请选择所属区域",
type: "string",
trigger: "blur",
},
],
// code: [
// {required: true, message: '请输入租户编码', type: 'string', trigger: 'blur'}
// ],
// type: [
// {required: true, message: '请选择租户类型', type: 'number', trigger: 'blur'}
// ],
// status: [
// {required: true, message: '请选择是否默认', type: 'number', trigger: 'blur'}
// ],
// sort: [
// {required: true, message: '请输入排序号', type: 'number', trigger: 'blur'}
// ],
username: [
{required: true, message: '请输入登录账号', type: 'string', trigger: 'blur'}
{
required: true,
message: "请输入登录账号",
type: "string",
trigger: "blur",
},
],
password: [
{required: true, message: '请输入登录密码', type: 'string', trigger: 'blur'}
{
required: true,
message: "请输入登录密码",
type: "string",
trigger: "blur",
},
],
platformName: [
{
required: true,
message: "请输入平台名称",
type: "string",
trigger: "blur",
},
]
},
provinceList: [],
cityList: [],
districtList: [],
// 提交状态
loading: false,
// 是否是修改
isUpdate: false,
// 图片上传目录
updDir: 'tenant',
};
initUpload: ''
}
},
watch: {
data() {
if (this.data) {
this.form = Object.assign({}, this.data);
this.isUpdate = true;
this.initUpload = this.data.logo

} else {
this.form = {status: 1};
this.form = { status: 1 };
this.isUpdate = false;
this.initUpload = ''
}
if (this.$refs.form) {
this.$refs.form.clearValidate();
}
},
visible() {
if(this.visible) {
this.getAreaList()
}
}
},
methods: {
/* 获取省市区列表 */
getAreaList() {
this.provinceList = []
this.$http.get('/city/queryCityList').then(res => {
this.provinceList = res.data
if(this.provinceList.length) {
if(this.data.provinceCode) {
this.provinceList.forEach((item) => {
if (item.citycode == this.data.provinceCode) {
this.cityList = item.itemList
}
})
}
if(this.data.cityCode) {
this.cityList.forEach((item) => {
if (item.citycode == this.data.cityCode) {
this.districtList = item.itemList
}
})
}
}
}).catch(e => {
this.$message.error(e.message)
})
},
/* 选择省市区 */
selectArea(value, type, name){
if (type === 'province') {
this.cityList = []
this.districtList = []
this.form.cityCode = null
this.form.districtCode = null
}
if (type === 'city') {
this.districtList = []
this.form.districtCode = null
}
this.getListBycode(value, type, name)
},
/* 根据选择的选中的code返回子项列表 */
getListBycode(code, name, childrenName) {
this[name + 'List'].forEach((item) => {
if (item.citycode == code) {
this[childrenName + 'List'] = item.itemList
this.form[name+'Name'] = item.name
}
})
},
/* 保存编辑 */
save() {
this.$refs.form.validate().then(() => {
this.loading = true;
this.$http[this.isUpdate ? 'put' : 'post'](this.isUpdate ? '/tenant/edit' : '/tenant/add', this.form).then(res => {
this.loading = false;
if (res.data.code === 0) {
this.$message.success(res.data.msg);
if (!this.isUpdate) {
this.form = {};
this.$refs.form
.validate()
.then(() => {
this.loading = true;
this.$refs.logoUpload.startUpload().then((res)=> {
if(!res.includes('error')) {
this.form.logo = res[0]
this.$http[this.isUpdate ? "put" : "post"](
this.isUpdate ? "/tenant/edit" : "/tenant/add",
this.form
)
.then((res) => {
this.loading = false;
if (res.data.code === 0) {
this.$message.success(res.data.msg);
if (!this.isUpdate) {
this.form = {};
}
this.updateVisible(false);
this.$emit("done");
} else {
this.$message.error(res.data.msg);
}
})
.catch((e) => {
this.loading = false;
this.$message.error(e.message);
});
}
this.updateVisible(false);
this.$emit('done');
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
this.loading = false;
this.$message.error(e.message);
});
}).catch(() => {
});
})
})
.catch(() => {});
},
/* 更新visible */
updateVisible(value) {
this.$emit('update:visible', value);
}
}
}
this.$emit("update:visible", value);
},
},
};
</script>

<style scoped>

Chargement…
Annuler
Enregistrer