diff --git a/src/router/modules/order.js b/src/router/modules/order.js new file mode 100644 index 0000000..d28741e --- /dev/null +++ b/src/router/modules/order.js @@ -0,0 +1,27 @@ +/** When your routing table is too long, you can split it into small modules**/ + +import Layout from '@/layout' + +const orderRouter = { + path: '/orders', + component: Layout, + redirect: '/order/page', + alwaysShow: true, // will always show the root menu + name: 'Order', + meta: { + title: 'orders', + icon: 'shopping', + roles: ['admin', 'assistant', 'runner', 'shoper'] // you can set roles in root nav + }, + children: [{ + path: 'page', + component: () => import('@/views/order/list'), + name: 'OrderList', + meta: { + title: 'OrderList', + roles: ['admin', 'assistant', 'runner', 'shoper'] // or you can only set roles in sub nav + } + }] +} + +export default orderRouter diff --git a/src/router/modules/sites.js b/src/router/modules/sites.js new file mode 100644 index 0000000..6ce6943 --- /dev/null +++ b/src/router/modules/sites.js @@ -0,0 +1,26 @@ +import Layout from '@/layout' + +const sitesRouter = { + path: '/sites', + component: Layout, + redirect: '/site/page', + alwaysShow: true, // will always show the root menu + name: 'Site', + meta: { + title: 'sites', + icon: 'people', + roles: ['admin', 'assistant', 'runner'] // you can set roles in root nav + }, + children: [{ + path: 'page', + component: () => import('@/views/site/list'), + name: 'SiteList', + meta: { + title: '站点列表', + roles: ['admin', 'runner'] + + } + }] +} + +export default sitesRouter diff --git a/src/views/dashboard/runner/index.vue b/src/views/dashboard/runner/index.vue new file mode 100755 index 0000000..e2085a6 --- /dev/null +++ b/src/views/dashboard/runner/index.vue @@ -0,0 +1,72 @@ +<template> + <div class="dashboard-editor-container"> + <div class="clearfix"> + <pan-thumb :image="avatar" style="float: left"> + Your roles: + <span v-for="item in roles" :key="item" class="pan-info-roles">{{ item }}</span> + </pan-thumb> + <github-corner style="position: absolute; top: 0px; border: 0; right: 0;" /> + <div class="info-container"> + <span class="display_name">{{ name }}</span> + <span style="font-size:20px;padding-top:20px;display:inline-block;">{{ roles }}'s Dashboard</span> + </div> + </div> + <div> + <img :src="emptyGif" class="emptyGif"> + </div> + </div> +</template> + +<script> +import { mapGetters } from 'vuex' +import PanThumb from '@/components/PanThumb' +// import GithubCorner from '@/components/GithubCorner' + +export default { + name: 'DashboardEditor', + // components: { PanThumb, GithubCorner }, + components: { PanThumb }, + data() { + return { + emptyGif: + 'https://wpimg.wallstcn.com/0e03b7da-db9e-4819-ba10-9016ddfdaed3' + } + }, + computed: { + ...mapGetters(['name', 'avatar', 'roles']) + } +} +</script> + +<style lang="scss" scoped> +.emptyGif { + display: block; + width: 45%; + margin: 0 auto; +} + +.dashboard-editor-container { + background-color: #e3e3e3; + min-height: 100vh; + padding: 50px 60px 0px; + .pan-info-roles { + font-size: 12px; + font-weight: 700; + color: #333; + display: block; + } + .info-container { + position: relative; + margin-left: 190px; + height: 150px; + line-height: 200px; + .display_name { + font-size: 48px; + line-height: 48px; + color: #212121; + position: absolute; + top: 25px; + } + } +} +</style> diff --git a/src/views/order/list.vue b/src/views/order/list.vue new file mode 100755 index 0000000..f67b8cf --- /dev/null +++ b/src/views/order/list.vue @@ -0,0 +1,621 @@ +<template> + <div class="app-container"> + <div class="filter-container"> + <el-input + v-model="listQuery.title" + :placeholder="$t('table.title')" + style="width: 200px;" + class="filter-item" + @keyup.enter.native="handleFilter" + /> + <el-select + v-model="listQuery.importance" + :placeholder="$t('table.importance')" + clearable + style="width: 90px" + class="filter-item" + > + <el-option + v-for="item in importanceOptions" + :key="item" + :label="item" + :value="item" + /> + </el-select> + <el-select + v-model="listQuery.type" + :placeholder="$t('table.type')" + clearable + class="filter-item" + style="width: 130px" + > + <el-option + v-for="item in calendarTypeOptions" + :key="item.key" + :label="item.display_name+'('+item.key+')'" + :value="item.key" + /> + </el-select> + <el-select + v-model="listQuery.sort" + style="width: 140px" + class="filter-item" + @change="handleFilter" + > + <el-option + v-for="item in sortOptions" + :key="item.key" + :label="item.label" + :value="item.key" + /> + </el-select> + <el-button + v-waves + class="filter-item" + type="primary" + icon="el-icon-search" + @click="handleFilter" + > + {{ $t('table.search') }} + </el-button> + <el-button + class="filter-item" + style="margin-left: 10px;" + type="primary" + icon="el-icon-edit" + @click="handleCreate" + > + {{ $t('table.add') }} + </el-button> + <el-button + v-waves + :loading="downloadLoading" + class="filter-item" + type="primary" + icon="el-icon-download" + @click="handleDownload" + > + {{ $t('table.export') }} + </el-button> + <el-checkbox + v-model="showReviewer" + class="filter-item" + style="margin-left:15px;" + @change="tableKey=tableKey+1" + > + {{ $t('table.reviewer') }} + </el-checkbox> + </div> + + <el-table + :key="tableKey" + v-loading="listLoading" + :data="list" + border + fit + highlight-current-row + style="width: 100%;" + @sort-change="sortChange" + > + <el-table-column + :label="$t('table.id')" + prop="id" + sortable="custom" + align="center" + width="80" + :class-name="getSortClass('id')" + > + <template slot-scope="{row}"> + <span>{{ row.id }}</span> + </template> + </el-table-column> + <el-table-column + :label="$t('table.date')" + width="150px" + align="center" + > + <template slot-scope="{row}"> + <span>{{ row.timestamp | parseTime('{y}-{m}-{d} {h}:{i}') }}</span> + </template> + </el-table-column> + <el-table-column + :label="$t('table.title')" + min-width="150px" + > + <template slot-scope="{row}"> + <span + class="link-type" + @click="handleUpdate(row)" + >{{ row.title }}</span> + <el-tag>{{ row.type | typeFilter }}</el-tag> + </template> + </el-table-column> + <el-table-column + :label="$t('table.author')" + width="110px" + align="center" + > + <template slot-scope="{row}"> + <span>{{ row.author }}</span> + </template> + </el-table-column> + <el-table-column + v-if="showReviewer" + :label="$t('table.reviewer')" + width="110px" + align="center" + > + <template slot-scope="{row}"> + <span style="color:red;">{{ row.reviewer }}</span> + </template> + </el-table-column> + <el-table-column + :label="$t('table.importance')" + width="80px" + > + <template slot-scope="{row}"> + <svg-icon + v-for="n in +row.importance" + :key="n" + icon-class="star" + class="meta-item__icon" + /> + </template> + </el-table-column> + <el-table-column + :label="$t('table.readings')" + align="center" + width="95" + > + <template slot-scope="{row}"> + <span + v-if="row.pageviews" + class="link-type" + @click="handleFetchPv(row.pageviews)" + >{{ row.pageviews }}</span> + <span v-else>0</span> + </template> + </el-table-column> + <el-table-column + :label="$t('table.status')" + class-name="status-col" + width="100" + > + <template slot-scope="{row}"> + <el-tag :type="row.status | statusFilter"> + {{ row.status }} + </el-tag> + </template> + </el-table-column> + <el-table-column + :label="$t('table.actions')" + align="center" + width="230" + class-name="small-padding fixed-width" + > + <template slot-scope="{row,$index}"> + <el-button + type="primary" + size="mini" + @click="handleUpdate(row)" + > + {{ $t('table.edit') }} + </el-button> + <el-button + v-if="row.status!='published'" + size="mini" + type="success" + @click="handleModifyStatus(row,'published')" + > + {{ $t('table.publish') }} + </el-button> + <el-button + v-if="row.status!='draft'" + size="mini" + @click="handleModifyStatus(row,'draft')" + > + {{ $t('table.draft') }} + </el-button> + <el-button + v-if="row.status!='deleted'" + size="mini" + type="danger" + @click="handleDelete(row,$index)" + > + {{ $t('table.delete') }} + </el-button> + </template> + </el-table-column> + </el-table> + + <pagination + v-show="total>0" + :total="total" + :page.sync="listQuery.page" + :limit.sync="listQuery.limit" + @pagination="getList" + /> + + <el-dialog + :title="textMap[dialogStatus]" + :visible.sync="dialogFormVisible" + > + <el-form + ref="dataForm" + :rules="rules" + :model="temp" + label-position="left" + label-width="70px" + style="width: 400px; margin-left:50px;" + > + <el-form-item + :label="$t('table.type')" + prop="type" + > + <el-select + v-model="temp.type" + class="filter-item" + placeholder="Please select" + > + <el-option + v-for="item in calendarTypeOptions" + :key="item.key" + :label="item.display_name" + :value="item.key" + /> + </el-select> + </el-form-item> + <el-form-item + :label="$t('table.date')" + prop="timestamp" + > + <el-date-picker + v-model="temp.timestamp" + type="datetime" + placeholder="Please pick a date" + /> + </el-form-item> + <el-form-item + :label="$t('table.title')" + prop="title" + > + <el-input v-model="temp.title" /> + </el-form-item> + <el-form-item :label="$t('table.status')"> + <el-select + v-model="temp.status" + class="filter-item" + placeholder="Please select" + > + <el-option + v-for="item in statusOptions" + :key="item" + :label="item" + :value="item" + /> + </el-select> + </el-form-item> + <el-form-item :label="$t('table.importance')"> + <el-rate + v-model="temp.importance" + :colors="['#99A9BF', '#F7BA2A', '#FF9900']" + :max="3" + style="margin-top:8px;" + /> + </el-form-item> + <el-form-item :label="$t('table.remark')"> + <el-input + v-model="temp.remark" + :autosize="{ minRows: 2, maxRows: 4}" + type="textarea" + placeholder="Please input" + /> + </el-form-item> + </el-form> + <div + slot="footer" + class="dialog-footer" + > + <el-button @click="dialogFormVisible = false"> + {{ $t('table.cancel') }} + </el-button> + <el-button + type="primary" + @click="dialogStatus==='create'?createData():updateData()" + > + {{ $t('table.confirm') }} + </el-button> + </div> + </el-dialog> + + <el-dialog + :visible.sync="dialogPvVisible" + title="Reading statistics" + > + <el-table + :data="pvData" + border + fit + highlight-current-row + style="width: 100%" + > + <el-table-column + prop="key" + label="Channel" + /> + <el-table-column + prop="pv" + label="Pv" + /> + </el-table> + <span + slot="footer" + class="dialog-footer" + > + <el-button + type="primary" + @click="dialogPvVisible = false" + >{{ $t('table.confirm') }}</el-button> + </span> + </el-dialog> + </div> +</template> + +<script> +import { + fetchList, + fetchPv, + createArticle, + updateArticle +} from '@/api/article' +import waves from '@/directive/waves' // waves directive +import { parseTime } from '@/utils' +import Pagination from '@/components/Pagination' // secondary package based on el-pagination + +const calendarTypeOptions = [ + { key: 'CN', display_name: 'China' }, + { key: 'US', display_name: 'USA' }, + { key: 'JP', display_name: 'Japan' }, + { key: 'EU', display_name: 'Eurozone' } +] + +// arr to obj, such as { CN : "China", US : "USA" } +const calendarTypeKeyValue = calendarTypeOptions.reduce((acc, cur) => { + acc[cur.key] = cur.display_name + return acc +}, {}) + +export default { + name: 'ComplexTable', + components: { Pagination }, + directives: { waves }, + filters: { + statusFilter(status) { + const statusMap = { + published: 'success', + draft: 'info', + deleted: 'danger' + } + return statusMap[status] + }, + typeFilter(type) { + return calendarTypeKeyValue[type] + } + }, + data() { + return { + tableKey: 0, + list: null, + total: 0, + listLoading: true, + listQuery: { + page: 1, + limit: 20, + importance: undefined, + title: undefined, + type: undefined, + sort: '+id' + }, + importanceOptions: [1, 2, 3], + calendarTypeOptions, + sortOptions: [ + { label: 'ID Ascending', key: '+id' }, + { label: 'ID Descending', key: '-id' } + ], + statusOptions: ['published', 'draft', 'deleted'], + showReviewer: false, + temp: { + id: undefined, + importance: 1, + remark: '', + timestamp: new Date(), + title: '', + type: '', + status: 'published' + }, + dialogFormVisible: false, + dialogStatus: '', + textMap: { + update: 'Edit', + create: 'Create' + }, + dialogPvVisible: false, + pvData: [], + rules: { + type: [ + { required: true, message: 'type is required', trigger: 'change' } + ], + timestamp: [ + { + type: 'date', + required: true, + message: 'timestamp is required', + trigger: 'change' + } + ], + title: [ + { required: true, message: 'title is required', trigger: 'blur' } + ] + }, + downloadLoading: false + } + }, + created() { + this.getList() + }, + methods: { + getList() { + this.listLoading = true + fetchList(this.listQuery).then(response => { + this.list = response.data.items + this.total = response.data.total + + // Just to simulate the time of the request + setTimeout(() => { + this.listLoading = false + }, 1.5 * 1000) + }) + }, + handleFilter() { + this.listQuery.page = 1 + this.getList() + }, + handleModifyStatus(row, status) { + this.$message({ + message: '操作成功', + type: 'success' + }) + row.status = status + }, + sortChange(data) { + const { prop, order } = data + if (prop === 'id') { + this.sortByID(order) + } + }, + sortByID(order) { + if (order === 'ascending') { + this.listQuery.sort = '+id' + } else { + this.listQuery.sort = '-id' + } + this.handleFilter() + }, + resetTemp() { + this.temp = { + id: undefined, + importance: 1, + remark: '', + timestamp: new Date(), + title: '', + status: 'published', + type: '' + } + }, + handleCreate() { + this.resetTemp() + this.dialogStatus = 'create' + this.dialogFormVisible = true + this.$nextTick(() => { + this.$refs['dataForm'].clearValidate() + }) + }, + createData() { + this.$refs['dataForm'].validate(valid => { + if (valid) { + this.temp.id = parseInt(Math.random() * 100) + 1024 // mock a id + this.temp.author = '秀野堂主' + createArticle(this.temp).then(() => { + this.list.unshift(this.temp) + this.dialogFormVisible = false + this.$notify({ + title: '成功', + message: '创建成功', + type: 'success', + duration: 2000 + }) + }) + } + }) + }, + handleUpdate(row) { + this.temp = Object.assign({}, row) // copy obj + this.temp.timestamp = new Date(this.temp.timestamp) + this.dialogStatus = 'update' + this.dialogFormVisible = true + this.$nextTick(() => { + this.$refs['dataForm'].clearValidate() + }) + }, + updateData() { + this.$refs['dataForm'].validate(valid => { + if (valid) { + const tempData = Object.assign({}, this.temp) + tempData.timestamp = +new Date(tempData.timestamp) // change Thu Nov 30 2017 16:41:05 GMT+0800 (CST) to 1512031311464 + updateArticle(tempData).then(() => { + const index = this.list.findIndex(v => v.id === this.temp.id) + this.list.splice(index, 1, this.temp) + this.dialogFormVisible = false + this.$notify({ + title: '成功', + message: '更新成功', + type: 'success', + duration: 2000 + }) + }) + } + }) + }, + handleDelete(row, index) { + this.$notify({ + title: '成功', + message: '删除成功', + type: 'success', + duration: 2000 + }) + this.list.splice(index, 1) + }, + handleFetchPv(pv) { + fetchPv(pv).then(response => { + this.pvData = response.data.pvData + this.dialogPvVisible = true + }) + }, + handleDownload() { + this.downloadLoading = true + import('@/vendor/Export2Excel').then(excel => { + const tHeader = ['timestamp', 'title', 'type', 'importance', 'status'] + const filterVal = [ + 'timestamp', + 'title', + 'type', + 'importance', + 'status' + ] + const data = this.formatJson(filterVal) + excel.export_json_to_excel({ + header: tHeader, + data, + filename: 'table-list' + }) + this.downloadLoading = false + }) + }, + formatJson(filterVal) { + return this.list.map(v => + filterVal.map(j => { + if (j === 'timestamp') { + return parseTime(v[j]) + } else { + return v[j] + } + }) + ) + }, + getSortClass: function(key) { + const sort = this.listQuery.sort + return sort === `+${key}` ? 'ascending' : 'descending' + } + } +} +</script> diff --git a/src/views/prod/list.vue b/src/views/prod/list.vue new file mode 100755 index 0000000..02cb401 --- /dev/null +++ b/src/views/prod/list.vue @@ -0,0 +1,203 @@ +<template> + <div class="app-container"> + <!-- Note that row-key is necessary to get a correct row order. --> + <el-table + ref="dragTable" + v-loading="listLoading" + :data="list" + row-key="id" + border + fit + highlight-current-row + style="width: 100%" + > + <el-table-column + align="center" + label="ID" + width="65" + > + <template slot-scope="{row}"> + <span>{{ row.id }}</span> + </template> + </el-table-column> + + <el-table-column + width="180px" + align="center" + label="Date" + > + <template slot-scope="{row}"> + <span>{{ row.timestamp | parseTime('{y}-{m}-{d} {h}:{i}') }}</span> + </template> + </el-table-column> + + <el-table-column + min-width="300px" + label="Title" + > + <template slot-scope="{row}"> + <span>{{ row.title }}</span> + </template> + </el-table-column> + + <el-table-column + width="110px" + align="center" + label="Author" + > + <template slot-scope="{row}"> + <span>{{ row.author }}</span> + </template> + </el-table-column> + + <el-table-column + width="100px" + label="Importance" + > + <template slot-scope="{row}"> + <svg-icon + v-for="n in + row.importance" + :key="n" + icon-class="star" + class="icon-star" + /> + </template> + </el-table-column> + + <el-table-column + align="center" + label="Readings" + width="95" + > + <template slot-scope="{row}"> + <span>{{ row.pageviews }}</span> + </template> + </el-table-column> + + <el-table-column + class-name="status-col" + label="Status" + width="110" + > + <template slot-scope="{row}"> + <el-tag :type="row.status | statusFilter"> + {{ row.status }} + </el-tag> + </template> + </el-table-column> + + <el-table-column + align="center" + label="Drag" + width="80" + > + <template slot-scope="{}"> + <svg-icon + class="drag-handler" + icon-class="drag" + /> + </template> + </el-table-column> + </el-table> + <!-- $t is vue-i18n global function to translate lang (lang in @/lang) --> + <div class="show-d"> + <el-tag style="margin-right:12px;">{{ $t('table.dragTips1') }} :</el-tag> {{ oldList }} + </div> + <div class="show-d"> + <el-tag>{{ $t('table.dragTips2') }} :</el-tag> {{ newList }} + </div> + </div> +</template> + +<script> +import { fetchList } from '@/api/article' +import Sortable from 'sortablejs' + +export default { + name: 'DragTable', + filters: { + statusFilter(status) { + const statusMap = { + published: 'success', + draft: 'info', + deleted: 'danger' + } + return statusMap[status] + } + }, + data() { + return { + list: null, + total: null, + listLoading: true, + listQuery: { + page: 1, + limit: 10 + }, + sortable: null, + oldList: [], + newList: [] + } + }, + created() { + this.getList() + }, + methods: { + async getList() { + this.listLoading = true + const { data } = await fetchList(this.listQuery) + this.list = data.items + this.total = data.total + this.listLoading = false + this.oldList = this.list.map(v => v.id) + this.newList = this.oldList.slice() + this.$nextTick(() => { + this.setSort() + }) + }, + setSort() { + const el = this.$refs.dragTable.$el.querySelectorAll( + '.el-table__body-wrapper > table > tbody' + )[0] + this.sortable = Sortable.create(el, { + ghostClass: 'sortable-ghost', // Class name for the drop placeholder, + setData: function(dataTransfer) { + // to avoid Firefox bug + // Detail see : https://github.com/RubaXa/Sortable/issues/1012 + dataTransfer.setData('Text', '') + }, + onEnd: evt => { + const targetRow = this.list.splice(evt.oldIndex, 1)[0] + this.list.splice(evt.newIndex, 0, targetRow) + + // for show the changes, you can delete in you code + const tempIndex = this.newList.splice(evt.oldIndex, 1)[0] + this.newList.splice(evt.newIndex, 0, tempIndex) + } + }) + } + } +} +</script> + +<style> +.sortable-ghost { + opacity: 0.8; + color: #fff !important; + background: #42b983 !important; +} +</style> + +<style scoped> +.icon-star { + margin-right: 2px; +} +.drag-handler { + width: 20px; + height: 20px; + cursor: pointer; +} +.show-d { + margin-top: 15px; +} +</style> diff --git a/src/views/site/list.vue b/src/views/site/list.vue new file mode 100644 index 0000000..a3fe0b7 --- /dev/null +++ b/src/views/site/list.vue @@ -0,0 +1,191 @@ +<template> + <div class="app-container"> + <el-table + v-loading="listLoading" + :data="list" + border + fit + highlight-current-row + style="width: 100%" + > + <el-table-column + align="center" + label="ID" + width="80" + > + <template slot-scope="{row}"> + <span>{{ row.id }}</span> + </template> + </el-table-column> + + <el-table-column + width="180px" + align="center" + label="Date" + > + <template slot-scope="{row}"> + <span>{{ row.timestamp | parseTime('{y}-{m}-{d} {h}:{i}') }}</span> + </template> + </el-table-column> + + <el-table-column + width="120px" + align="center" + label="Author" + > + <template slot-scope="{row}"> + <span>{{ row.author }}</span> + </template> + </el-table-column> + + <el-table-column + width="100px" + label="Importance" + > + <template slot-scope="{row}"> + <svg-icon + v-for="n in + row.importance" + :key="n" + icon-class="star" + class="meta-item__icon" + /> + </template> + </el-table-column> + + <el-table-column + class-name="status-col" + label="Status" + width="110" + > + <template slot-scope="{row}"> + <el-tag :type="row.status | statusFilter"> + {{ row.status }} + </el-tag> + </template> + </el-table-column> + + <el-table-column + min-width="300px" + label="Title" + > + <template slot-scope="{row}"> + <template v-if="row.edit"> + <el-input + v-model="row.title" + class="edit-input" + size="small" + /> + <el-button + class="cancel-btn" + size="small" + icon="el-icon-refresh" + type="warning" + @click="cancelEdit(row)" + > + cancel + </el-button> + </template> + <span v-else>{{ row.title }}</span> + </template> + </el-table-column> + + <el-table-column + align="center" + label="Actions" + width="120" + > + <template slot-scope="{row}"> + <el-button + v-if="row.edit" + type="success" + size="small" + icon="el-icon-circle-check-outline" + @click="confirmEdit(row)" + > + Ok + </el-button> + <el-button + v-else + type="primary" + size="small" + icon="el-icon-edit" + @click="row.edit=!row.edit" + > + Edit + </el-button> + </template> + </el-table-column> + </el-table> + </div> +</template> + +<script> +import { fetchList } from '@/api/article' + +export default { + name: 'InlineEditTable', + filters: { + statusFilter(status) { + const statusMap = { + published: 'success', + draft: 'info', + deleted: 'danger' + } + return statusMap[status] + } + }, + data() { + return { + list: null, + listLoading: true, + listQuery: { + page: 1, + limit: 10 + } + } + }, + created() { + this.getList() + }, + methods: { + async getList() { + this.listLoading = true + const { data } = await fetchList(this.listQuery) + const items = data.items + this.list = items.map(v => { + this.$set(v, 'edit', false) // https://vuejs.org/v2/guide/reactivity.html + v.originalTitle = v.title // will be used when user click the cancel botton + return v + }) + this.listLoading = false + }, + cancelEdit(row) { + row.title = row.originalTitle + row.edit = false + this.$message({ + message: 'The title has been restored to the original value', + type: 'warning' + }) + }, + confirmEdit(row) { + row.edit = false + row.originalTitle = row.title + this.$message({ + message: 'The title has been edited', + type: 'success' + }) + } + } +} +</script> + +<style scoped> +.edit-input { + padding-right: 100px; +} +.cancel-btn { + position: absolute; + right: 15px; + top: 10px; +} +</style>