master
wocao 2 years ago
parent ad49d324ee
commit 910eb9b44c

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2017-present PanJiaChen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -1,116 +0,0 @@
import Mock from 'mockjs'
const List = []
const count = 100
const baseContent = '<p>I am testing data, I am testing data.</p><p><img src="https://wpimg.wallstcn.com/4c69009c-0fd4-4153-b112-6cb53d1cf943"></p>'
const image_uri = 'https://wpimg.wallstcn.com/e4558086-631c-425c-9430-56ffb46e70b3'
for (let i = 0; i < count; i++) {
List.push(Mock.mock({
id: '@increment',
timestamp: +Mock.Random.date('T'),
author: '@first',
reviewer: '@first',
title: '@title(5, 10)',
content_short: 'mock data',
content: baseContent,
forecast: '@float(0, 100, 2, 2)',
importance: '@integer(1, 3)',
'type|1': ['CN', 'US', 'JP', 'EU'],
'status|1': ['published', 'draft', 'deleted'],
display_time: '@datetime',
comment_disabled: true,
pageviews: '@integer(300, 5000)',
image_uri,
platforms: ['a-platform']
}))
}
export default [
{
url: '/article/list',
type: 'get',
response: config => {
const { importance, type, title, page = 1, limit = 20, sort } = config.query
let mockList = List.filter(item => {
if (importance && item.importance !== +importance) return false
if (type && item.type !== type) return false
if (title && item.title.indexOf(title) < 0) return false
return true
})
if (sort === '-id') {
mockList = mockList.reverse()
}
const pageList = mockList.filter((item, index) => index < limit * page && index >= limit * (page - 1))
return {
code: 20000,
data: {
total: mockList.length,
items: pageList
}
}
}
},
{
url: '/article/detail',
type: 'get',
response: config => {
const { id } = config.query
for (const article of List) {
if (article.id === +id) {
return {
code: 20000,
data: article
}
}
}
}
},
{
url: '/article/pv',
type: 'get',
response: _ => {
return {
code: 20000,
data: {
pvData: [
{ key: 'PC', pv: 1024 },
{ key: 'mobile', pv: 1024 },
{ key: 'ios', pv: 1024 },
{ key: 'android', pv: 1024 }
]
}
}
}
},
{
url: '/article/create',
type: 'post',
response: _ => {
return {
code: 20000,
data: 'success'
}
}
},
{
url: '/article/update',
type: 'post',
response: _ => {
return {
code: 20000,
data: 'success'
}
}
}
]

@ -1,70 +0,0 @@
import Mock from 'mockjs'
import { param2Obj } from '../src/utils'
import user from './user'
import role from './role'
import article from './article'
import search from './remote-search'
const mocks = [
...user,
...role,
...article,
...search
]
// for front mock
// please use it cautiously, it will redefine XMLHttpRequest,
// which will cause many of your third-party libraries to be invalidated(like progress event).
export function mockXHR() {
// mock patch
// https://github.com/nuysoft/Mock/issues/300
Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
Mock.XHR.prototype.send = function() {
if (this.custom.xhr) {
this.custom.xhr.withCredentials = this.withCredentials || false
if (this.responseType) {
this.custom.xhr.responseType = this.responseType
}
}
this.proxy_send(...arguments)
}
function XHR2ExpressReqWrap(respond) {
return function(options) {
let result = null
if (respond instanceof Function) {
const { body, type, url } = options
// https://expressjs.com/en/4x/api.html#req
result = respond({
method: type,
body: JSON.parse(body),
query: param2Obj(url)
})
} else {
result = respond
}
return Mock.mock(result)
}
}
for (const i of mocks) {
Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response))
}
}
// for mock server
const responseFake = (url, type, respond) => {
return {
url: new RegExp(`/mock${url}`),
type: type || 'get',
response(req, res) {
res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))
}
}
}
export default mocks.map(route => {
return responseFake(route.url, route.type, route.response)
})

@ -1,68 +0,0 @@
const chokidar = require('chokidar')
const bodyParser = require('body-parser')
const chalk = require('chalk')
const path = require('path')
const mockDir = path.join(process.cwd(), 'mock')
function registerRoutes(app) {
let mockLastIndex
const { default: mocks } = require('./index.js')
for (const mock of mocks) {
app[mock.type](mock.url, mock.response)
mockLastIndex = app._router.stack.length
}
const mockRoutesLength = Object.keys(mocks).length
return {
mockRoutesLength: mockRoutesLength,
mockStartIndex: mockLastIndex - mockRoutesLength
}
}
function unregisterRoutes() {
Object.keys(require.cache).forEach(i => {
if (i.includes(mockDir)) {
delete require.cache[require.resolve(i)]
}
})
}
module.exports = app => {
// es6 polyfill
require('@babel/register')
// parse app.body
// https://expressjs.com/en/4x/api.html#req.body
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({
extended: true
}))
const mockRoutes = registerRoutes(app)
var mockRoutesLength = mockRoutes.mockRoutesLength
var mockStartIndex = mockRoutes.mockStartIndex
// watch files, hot reload mock server
chokidar.watch(mockDir, {
ignored: /mock-server/,
ignoreInitial: true
}).on('all', (event, path) => {
if (event === 'change' || event === 'add') {
try {
// remove mock routes stack
app._router.stack.splice(mockStartIndex, mockRoutesLength)
// clear routes cache
unregisterRoutes()
const mockRoutes = registerRoutes(app)
mockRoutesLength = mockRoutes.mockRoutesLength
mockStartIndex = mockRoutes.mockStartIndex
console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`))
} catch (error) {
console.log(chalk.redBright(error))
}
}
})
}

@ -1,51 +0,0 @@
import Mock from 'mockjs'
const NameList = []
const count = 100
for (let i = 0; i < count; i++) {
NameList.push(Mock.mock({
name: '@first'
}))
}
NameList.push({ name: 'mock-Pan' })
export default [
// username search
{
url: '/search/user',
type: 'get',
response: config => {
const { name } = config.query
const mockNameList = NameList.filter(item => {
const lowerCaseName = item.name.toLowerCase()
return !(name && lowerCaseName.indexOf(name.toLowerCase()) < 0)
})
return {
code: 20000,
data: { items: mockNameList }
}
}
},
// transaction list
{
url: '/transaction/list',
type: 'get',
response: _ => {
return {
code: 20000,
data: {
total: 20,
'items|20': [{
order_no: '@guid()',
timestamp: +Mock.Random.date('T'),
username: '@name()',
price: '@float(1000, 15000, 0, 2)',
'status|1': ['success', 'pending']
}]
}
}
}
}
]

@ -1,98 +0,0 @@
import Mock from 'mockjs'
import { deepClone } from '../../src/utils/index.js'
import { asyncRoutes, constantRoutes } from './routes.js'
const routes = deepClone([...constantRoutes, ...asyncRoutes])
const roles = [
{
key: 'admin',
name: 'admin',
description: 'Super Administrator. Have access to view all pages.',
routes: routes
},
{
key: 'editor',
name: 'editor',
description: 'Normal Editor. Can see all pages except permission page',
routes: routes.filter(i => i.path !== '/permission')// just a mock
},
{
key: 'visitor',
name: 'visitor',
description: 'Just a visitor. Can only see the home page and the document page',
routes: [{
path: '',
redirect: 'dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
meta: { title: 'dashboard', icon: 'dashboard' }
}
]
}]
}
]
export default [
// mock get all routes form server
{
url: '/routes',
type: 'get',
response: _ => {
return {
code: 20000,
data: routes
}
}
},
// mock get all roles form server
{
url: '/roles',
type: 'get',
response: _ => {
return {
code: 20000,
data: roles
}
}
},
// add role
{
url: '/role',
type: 'post',
response: {
code: 20000,
data: {
key: Mock.mock('@integer(300, 5000)')
}
}
},
// update role
{
url: '/role/[A-Za-z0-9]',
type: 'put',
response: {
code: 20000,
data: {
status: 'success'
}
}
},
// delete role
{
url: '/role/[A-Za-z0-9]',
type: 'delete',
response: {
code: 20000,
data: {
status: 'success'
}
}
}
]

@ -1,525 +0,0 @@
// Just a mock data
export const constantRoutes = [
{
path: '/redirect',
component: 'layout/Layout',
hidden: true,
children: [
{
path: '/redirect/:path*',
component: 'views/redirect/index'
}
]
},
{
path: '/login',
component: 'views/login/index',
hidden: true
},
{
path: '/auth-redirect',
component: 'views/login/auth-redirect',
hidden: true
},
{
path: '/404',
component: 'views/error-page/404',
hidden: true
},
{
path: '/401',
component: 'views/error-page/401',
hidden: true
},
{
path: '',
component: 'layout/Layout',
redirect: 'dashboard',
children: [
{
path: 'dashboard',
component: 'views/dashboard/index',
name: 'Dashboard',
meta: { title: 'Dashboard', icon: 'dashboard', affix: true }
}
]
},
{
path: '/documentation',
component: 'layout/Layout',
children: [
{
path: 'index',
component: 'views/documentation/index',
name: 'Documentation',
meta: { title: 'Documentation', icon: 'documentation', affix: true }
}
]
},
{
path: '/guide',
component: 'layout/Layout',
redirect: '/guide/index',
children: [
{
path: 'index',
component: 'views/guide/index',
name: 'Guide',
meta: { title: 'Guide', icon: 'guide', noCache: true }
}
]
}
]
export const asyncRoutes = [
{
path: '/permission',
component: 'layout/Layout',
redirect: '/permission/index',
alwaysShow: true,
meta: {
title: 'Permission',
icon: 'lock',
roles: ['admin', 'editor']
},
children: [
{
path: 'page',
component: 'views/permission/page',
name: 'PagePermission',
meta: {
title: 'Page Permission',
roles: ['admin']
}
},
{
path: 'directive',
component: 'views/permission/directive',
name: 'DirectivePermission',
meta: {
title: 'Directive Permission'
}
},
{
path: 'role',
component: 'views/permission/role',
name: 'RolePermission',
meta: {
title: 'Role Permission',
roles: ['admin']
}
}
]
},
{
path: '/icon',
component: 'layout/Layout',
children: [
{
path: 'index',
component: 'views/icons/index',
name: 'Icons',
meta: { title: 'Icons', icon: 'icon', noCache: true }
}
]
},
{
path: '/components',
component: 'layout/Layout',
redirect: 'noRedirect',
name: 'ComponentDemo',
meta: {
title: 'Components',
icon: 'component'
},
children: [
{
path: 'tinymce',
component: 'views/components-demo/tinymce',
name: 'TinymceDemo',
meta: { title: 'Tinymce' }
},
{
path: 'markdown',
component: 'views/components-demo/markdown',
name: 'MarkdownDemo',
meta: { title: 'Markdown' }
},
{
path: 'json-editor',
component: 'views/components-demo/json-editor',
name: 'JsonEditorDemo',
meta: { title: 'Json Editor' }
},
{
path: 'split-pane',
component: 'views/components-demo/split-pane',
name: 'SplitpaneDemo',
meta: { title: 'SplitPane' }
},
{
path: 'avatar-upload',
component: 'views/components-demo/avatar-upload',
name: 'AvatarUploadDemo',
meta: { title: 'Avatar Upload' }
},
{
path: 'dropzone',
component: 'views/components-demo/dropzone',
name: 'DropzoneDemo',
meta: { title: 'Dropzone' }
},
{
path: 'sticky',
component: 'views/components-demo/sticky',
name: 'StickyDemo',
meta: { title: 'Sticky' }
},
{
path: 'count-to',
component: 'views/components-demo/count-to',
name: 'CountToDemo',
meta: { title: 'Count To' }
},
{
path: 'mixin',
component: 'views/components-demo/mixin',
name: 'ComponentMixinDemo',
meta: { title: 'componentMixin' }
},
{
path: 'back-to-top',
component: 'views/components-demo/back-to-top',
name: 'BackToTopDemo',
meta: { title: 'Back To Top' }
},
{
path: 'drag-dialog',
component: 'views/components-demo/drag-dialog',
name: 'DragDialogDemo',
meta: { title: 'Drag Dialog' }
},
{
path: 'drag-select',
component: 'views/components-demo/drag-select',
name: 'DragSelectDemo',
meta: { title: 'Drag Select' }
},
{
path: 'dnd-list',
component: 'views/components-demo/dnd-list',
name: 'DndListDemo',
meta: { title: 'Dnd List' }
},
{
path: 'drag-kanban',
component: 'views/components-demo/drag-kanban',
name: 'DragKanbanDemo',
meta: { title: 'Drag Kanban' }
}
]
},
{
path: '/charts',
component: 'layout/Layout',
redirect: 'noRedirect',
name: 'Charts',
meta: {
title: 'Charts',
icon: 'chart'
},
children: [
{
path: 'keyboard',
component: 'views/charts/keyboard',
name: 'KeyboardChart',
meta: { title: 'Keyboard Chart', noCache: true }
},
{
path: 'line',
component: 'views/charts/line',
name: 'LineChart',
meta: { title: 'Line Chart', noCache: true }
},
{
path: 'mixchart',
component: 'views/charts/mixChart',
name: 'MixChart',
meta: { title: 'Mix Chart', noCache: true }
}
]
},
{
path: '/nested',
component: 'layout/Layout',
redirect: '/nested/menu1/menu1-1',
name: 'Nested',
meta: {
title: 'Nested',
icon: 'nested'
},
children: [
{
path: 'menu1',
component: 'views/nested/menu1/index',
name: 'Menu1',
meta: { title: 'Menu1' },
redirect: '/nested/menu1/menu1-1',
children: [
{
path: 'menu1-1',
component: 'views/nested/menu1/menu1-1',
name: 'Menu1-1',
meta: { title: 'Menu1-1' }
},
{
path: 'menu1-2',
component: 'views/nested/menu1/menu1-2',
name: 'Menu1-2',
redirect: '/nested/menu1/menu1-2/menu1-2-1',
meta: { title: 'Menu1-2' },
children: [
{
path: 'menu1-2-1',
component: 'views/nested/menu1/menu1-2/menu1-2-1',
name: 'Menu1-2-1',
meta: { title: 'Menu1-2-1' }
},
{
path: 'menu1-2-2',
component: 'views/nested/menu1/menu1-2/menu1-2-2',
name: 'Menu1-2-2',
meta: { title: 'Menu1-2-2' }
}
]
},
{
path: 'menu1-3',
component: 'views/nested/menu1/menu1-3',
name: 'Menu1-3',
meta: { title: 'Menu1-3' }
}
]
},
{
path: 'menu2',
name: 'Menu2',
component: 'views/nested/menu2/index',
meta: { title: 'Menu2' }
}
]
},
{
path: '/example',
component: 'layout/Layout',
redirect: '/example/list',
name: 'Example',
meta: {
title: 'Example',
icon: 'example'
},
children: [
{
path: 'create',
component: 'views/example/create',
name: 'CreateArticle',
meta: { title: 'Create Article', icon: 'edit' }
},
{
path: 'edit/:id(\\d+)',
component: 'views/example/edit',
name: 'EditArticle',
meta: { title: 'Edit Article', noCache: true },
hidden: true
},
{
path: 'list',
component: 'views/example/list',
name: 'ArticleList',
meta: { title: 'Article List', icon: 'list' }
}
]
},
{
path: '/tab',
component: 'layout/Layout',
children: [
{
path: 'index',
component: 'views/tab/index',
name: 'Tab',
meta: { title: 'Tab', icon: 'tab' }
}
]
},
{
path: '/error',
component: 'layout/Layout',
redirect: 'noRedirect',
name: 'ErrorPages',
meta: {
title: 'Error Pages',
icon: '404'
},
children: [
{
path: '401',
component: 'views/error-page/401',
name: 'Page401',
meta: { title: 'Page 401', noCache: true }
},
{
path: '404',
component: 'views/error-page/404',
name: 'Page404',
meta: { title: 'Page 404', noCache: true }
}
]
},
{
path: '/error-log',
component: 'layout/Layout',
redirect: 'noRedirect',
children: [
{
path: 'log',
component: 'views/error-log/index',
name: 'ErrorLog',
meta: { title: 'Error Log', icon: 'bug' }
}
]
},
{
path: '/excel',
component: 'layout/Layout',
redirect: '/excel/export-excel',
name: 'Excel',
meta: {
title: 'Excel',
icon: 'excel'
},
children: [
{
path: 'export-excel',
component: 'views/excel/export-excel',
name: 'ExportExcel',
meta: { title: 'Export Excel' }
},
{
path: 'export-selected-excel',
component: 'views/excel/select-excel',
name: 'SelectExcel',
meta: { title: 'Select Excel' }
},
{
path: 'export-merge-header',
component: 'views/excel/merge-header',
name: 'MergeHeader',
meta: { title: 'Merge Header' }
},
{
path: 'upload-excel',
component: 'views/excel/upload-excel',
name: 'UploadExcel',
meta: { title: 'Upload Excel' }
}
]
},
{
path: '/zip',
component: 'layout/Layout',
redirect: '/zip/download',
alwaysShow: true,
meta: { title: 'Zip', icon: 'zip' },
children: [
{
path: 'download',
component: 'views/zip/index',
name: 'ExportZip',
meta: { title: 'Export Zip' }
}
]
},
{
path: '/pdf',
component: 'layout/Layout',
redirect: '/pdf/index',
children: [
{
path: 'index',
component: 'views/pdf/index',
name: 'PDF',
meta: { title: 'PDF', icon: 'pdf' }
}
]
},
{
path: '/pdf/download',
component: 'views/pdf/download',
hidden: true
},
{
path: '/theme',
component: 'layout/Layout',
redirect: 'noRedirect',
children: [
{
path: 'index',
component: 'views/theme/index',
name: 'Theme',
meta: { title: 'Theme', icon: 'theme' }
}
]
},
{
path: '/clipboard',
component: 'layout/Layout',
redirect: 'noRedirect',
children: [
{
path: 'index',
component: 'views/clipboard/index',
name: 'ClipboardDemo',
meta: { title: 'Clipboard Demo', icon: 'clipboard' }
}
]
},
{
path: '/i18n',
component: 'layout/Layout',
children: [
{
path: 'index',
component: 'views/i18n-demo/index',
name: 'I18n',
meta: { title: 'I18n', icon: 'international' }
}
]
},
{
path: 'external-link',
component: 'layout/Layout',
children: [
{
path: 'https://github.com/PanJiaChen/vue-element-admin',
meta: { title: 'External Link', icon: 'link' }
}
]
},
{ path: '*', redirect: '/404', hidden: true }
]

@ -1,84 +0,0 @@
const tokens = {
admin: {
token: 'admin-token'
},
editor: {
token: 'editor-token'
}
}
const users = {
'admin-token': {
roles: ['admin'],
introduction: 'I am a super administrator',
avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
name: 'Super Admin'
},
'editor-token': {
roles: ['editor'],
introduction: 'I am an editor',
avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
name: 'Normal Editor'
}
}
export default [
// user login
{
url: '/user/login',
type: 'post',
response: config => {
const { username } = config.body
const token = tokens[username]
// mock error
if (!token) {
return {
code: 60204,
message: 'Account and password are incorrect.'
}
}
return {
code: 20000,
data: token
}
}
},
// get user info
{
url: '/user/info\.*',
type: 'get',
response: config => {
const { token } = config.query
const info = users[token]
// mock error
if (!info) {
return {
code: 50008,
message: 'Login failed, unable to get user details.'
}
}
return {
code: 20000,
data: info
}
}
},
// user logout
{
url: '/user/logout',
type: 'post',
response: _ => {
return {
code: 20000,
data: 'success'
}
}
}
]

@ -1,26 +0,0 @@
{{#if template}}
<template>
<div />
</template>
{{/if}}
{{#if script}}
<script>
export default {
name: '{{ properCase name }}',
props: {},
data() {
return {}
},
created() {},
mounted() {},
methods: {}
}
</script>
{{/if}}
{{#if style}}
<style lang="scss" scoped>
</style>
{{/if}}

@ -1,55 +0,0 @@
const { notEmpty } = require('../utils.js')
module.exports = {
description: 'generate vue component',
prompts: [{
type: 'input',
name: 'name',
message: 'component name please',
validate: notEmpty('name')
},
{
type: 'checkbox',
name: 'blocks',
message: 'Blocks:',
choices: [{
name: '<template>',
value: 'template',
checked: true
},
{
name: '<script>',
value: 'script',
checked: true
},
{
name: 'style',
value: 'style',
checked: true
}
],
validate(value) {
if (value.indexOf('script') === -1 && value.indexOf('template') === -1) {
return 'Components require at least a <script> or <template> tag.'
}
return true
}
}
],
actions: data => {
const name = '{{properCase name}}'
const actions = [{
type: 'add',
path: `src/components/${name}/index.vue`,
templateFile: 'plop-templates/component/index.hbs',
data: {
name: name,
template: data.blocks.includes('template'),
script: data.blocks.includes('script'),
style: data.blocks.includes('style')
}
}]
return actions
}
}

@ -1,9 +0,0 @@
exports.notEmpty = name => {
return v => {
if (!v || v.trim === '') {
return `${name} is required`
} else {
return true
}
}
}

@ -1,26 +0,0 @@
{{#if template}}
<template>
<div />
</template>
{{/if}}
{{#if script}}
<script>
export default {
name: '{{ properCase name }}',
props: {},
data() {
return {}
},
created() {},
mounted() {},
methods: {}
}
</script>
{{/if}}
{{#if style}}
<style lang="scss" scoped>
</style>
{{/if}}

@ -1,55 +0,0 @@
const { notEmpty } = require('../utils.js')
module.exports = {
description: 'generate a view',
prompts: [{
type: 'input',
name: 'name',
message: 'view name please',
validate: notEmpty('name')
},
{
type: 'checkbox',
name: 'blocks',
message: 'Blocks:',
choices: [{
name: '<template>',
value: 'template',
checked: true
},
{
name: '<script>',
value: 'script',
checked: true
},
{
name: 'style',
value: 'style',
checked: true
}
],
validate(value) {
if (value.indexOf('script') === -1 && value.indexOf('template') === -1) {
return 'View require at least a <script> or <template> tag.'
}
return true
}
}
],
actions: data => {
const name = '{{name}}'
const actions = [{
type: 'add',
path: `src/views/${name}/index.vue`,
templateFile: 'plop-templates/view/index.hbs',
data: {
name: name,
template: data.blocks.includes('template'),
script: data.blocks.includes('script'),
style: data.blocks.includes('style')
}
}]
return actions
}
}

@ -1,14 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="renderer" content="webkit">
<link rel="shortcut icon" href="//img.alicdn.com/imgextra/i2/O1CN01ZyAlrn1MwaMhqz36G_!!6000000001499-73-tps-64-64.ico" type="image/x-icon"/>
<title><%= webpackConfig.name %></title>
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
Loading…
Cancel
Save