user-detail

This commit is contained in:
xiaomlove
2021-04-27 19:13:32 +08:00
parent d1b7561aae
commit d2e05c812e
20 changed files with 594 additions and 50 deletions

128
admin/package-lock.json generated
View File

@@ -126,6 +126,16 @@
"resolved": "https://registry.npm.taobao.org/@vue/shared/download/@vue/shared-3.0.11.tgz?cache=0&sync_timestamp=1617321555263&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40vue%2Fshared%2Fdownload%2F%40vue%2Fshared-3.0.11.tgz", "resolved": "https://registry.npm.taobao.org/@vue/shared/download/@vue/shared-3.0.11.tgz?cache=0&sync_timestamp=1617321555263&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40vue%2Fshared%2Fdownload%2F%40vue%2Fshared-3.0.11.tgz",
"integrity": "sha1-INIt0Np9NYuyHBf5vehigVJkLHc=" "integrity": "sha1-INIt0Np9NYuyHBf5vehigVJkLHc="
}, },
"anymatch": {
"version": "3.1.2",
"resolved": "https://registry.npm.taobao.org/anymatch/download/anymatch-3.1.2.tgz",
"integrity": "sha1-wFV8CWrzLxBhmPT04qODU343hxY=",
"dev": true,
"requires": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
}
},
"async-validator": { "async-validator": {
"version": "3.5.1", "version": "3.5.1",
"resolved": "https://registry.npm.taobao.org/async-validator/download/async-validator-3.5.1.tgz?cache=0&sync_timestamp=1605749896979&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fasync-validator%2Fdownload%2Fasync-validator-3.5.1.tgz", "resolved": "https://registry.npm.taobao.org/async-validator/download/async-validator-3.5.1.tgz?cache=0&sync_timestamp=1605749896979&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fasync-validator%2Fdownload%2Fasync-validator-3.5.1.tgz",
@@ -145,12 +155,43 @@
"integrity": "sha1-ZfCvOC9Xi83HQr2cKB6cstd2gyg=", "integrity": "sha1-ZfCvOC9Xi83HQr2cKB6cstd2gyg=",
"dev": true "dev": true
}, },
"binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npm.taobao.org/binary-extensions/download/binary-extensions-2.2.0.tgz?cache=0&sync_timestamp=1610299308660&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fbinary-extensions%2Fdownload%2Fbinary-extensions-2.2.0.tgz",
"integrity": "sha1-dfUC7q+f/eQvyYgpZFvk6na9ni0=",
"dev": true
},
"bluebird": { "bluebird": {
"version": "3.7.2", "version": "3.7.2",
"resolved": "https://registry.npm.taobao.org/bluebird/download/bluebird-3.7.2.tgz?cache=0&sync_timestamp=1589682744631&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fbluebird%2Fdownload%2Fbluebird-3.7.2.tgz", "resolved": "https://registry.npm.taobao.org/bluebird/download/bluebird-3.7.2.tgz?cache=0&sync_timestamp=1589682744631&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fbluebird%2Fdownload%2Fbluebird-3.7.2.tgz",
"integrity": "sha1-nyKcFb4nJFT/qXOs4NvueaGww28=", "integrity": "sha1-nyKcFb4nJFT/qXOs4NvueaGww28=",
"dev": true "dev": true
}, },
"braces": {
"version": "3.0.2",
"resolved": "https://registry.npm.taobao.org/braces/download/braces-3.0.2.tgz",
"integrity": "sha1-NFThpGLujVmeI23zNs2epPiv4Qc=",
"dev": true,
"requires": {
"fill-range": "^7.0.1"
}
},
"chokidar": {
"version": "3.5.1",
"resolved": "https://registry.npm.taobao.org/chokidar/download/chokidar-3.5.1.tgz?cache=0&sync_timestamp=1610719430924&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fchokidar%2Fdownload%2Fchokidar-3.5.1.tgz",
"integrity": "sha1-7pznu+vSt59J8wR5nVRo4x4U5oo=",
"dev": true,
"requires": {
"anymatch": "~3.1.1",
"braces": "~3.0.2",
"fsevents": "~2.3.1",
"glob-parent": "~5.1.0",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.5.0"
}
},
"colorette": { "colorette": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npm.taobao.org/colorette/download/colorette-1.2.2.tgz?cache=0&sync_timestamp=1614259623635&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcolorette%2Fdownload%2Fcolorette-1.2.2.tgz", "resolved": "https://registry.npm.taobao.org/colorette/download/colorette-1.2.2.tgz?cache=0&sync_timestamp=1614259623635&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcolorette%2Fdownload%2Fcolorette-1.2.2.tgz",
@@ -214,6 +255,15 @@
"resolved": "https://registry.npm.taobao.org/estree-walker/download/estree-walker-2.0.2.tgz?cache=0&sync_timestamp=1611956983677&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Festree-walker%2Fdownload%2Festree-walker-2.0.2.tgz", "resolved": "https://registry.npm.taobao.org/estree-walker/download/estree-walker-2.0.2.tgz?cache=0&sync_timestamp=1611956983677&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Festree-walker%2Fdownload%2Festree-walker-2.0.2.tgz",
"integrity": "sha1-UvAQF4wqTBF6d1fP6UKtt9LaTKw=" "integrity": "sha1-UvAQF4wqTBF6d1fP6UKtt9LaTKw="
}, },
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npm.taobao.org/fill-range/download/fill-range-7.0.1.tgz",
"integrity": "sha1-GRmmp8df44ssfHflGYU12prN2kA=",
"dev": true,
"requires": {
"to-regex-range": "^5.0.1"
}
},
"follow-redirects": { "follow-redirects": {
"version": "1.13.3", "version": "1.13.3",
"resolved": "https://registry.npm.taobao.org/follow-redirects/download/follow-redirects-1.13.3.tgz", "resolved": "https://registry.npm.taobao.org/follow-redirects/download/follow-redirects-1.13.3.tgz",
@@ -241,6 +291,15 @@
"loader-utils": "^1.1.0" "loader-utils": "^1.1.0"
} }
}, },
"glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npm.taobao.org/glob-parent/download/glob-parent-5.1.2.tgz?cache=0&sync_timestamp=1615065997979&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fglob-parent%2Fdownload%2Fglob-parent-5.1.2.tgz",
"integrity": "sha1-hpgyxYA0/mikCTwX3BXoNA2EAcQ=",
"dev": true,
"requires": {
"is-glob": "^4.0.1"
}
},
"has": { "has": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npm.taobao.org/has/download/has-1.0.3.tgz", "resolved": "https://registry.npm.taobao.org/has/download/has-1.0.3.tgz",
@@ -268,6 +327,15 @@
"integrity": "sha1-xr5oWKvQE9do6YNmrkfiXViHsa4=", "integrity": "sha1-xr5oWKvQE9do6YNmrkfiXViHsa4=",
"dev": true "dev": true
}, },
"is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npm.taobao.org/is-binary-path/download/is-binary-path-2.1.0.tgz",
"integrity": "sha1-6h9/O4DwZCNug0cPhsCcJU+0Wwk=",
"dev": true,
"requires": {
"binary-extensions": "^2.0.0"
}
},
"is-core-module": { "is-core-module": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npm.taobao.org/is-core-module/download/is-core-module-2.2.0.tgz?cache=0&sync_timestamp=1606411622542&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fis-core-module%2Fdownload%2Fis-core-module-2.2.0.tgz", "resolved": "https://registry.npm.taobao.org/is-core-module/download/is-core-module-2.2.0.tgz?cache=0&sync_timestamp=1606411622542&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fis-core-module%2Fdownload%2Fis-core-module-2.2.0.tgz",
@@ -277,6 +345,27 @@
"has": "^1.0.3" "has": "^1.0.3"
} }
}, },
"is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npm.taobao.org/is-extglob/download/is-extglob-2.1.1.tgz",
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
"dev": true
},
"is-glob": {
"version": "4.0.1",
"resolved": "https://registry.npm.taobao.org/is-glob/download/is-glob-4.0.1.tgz",
"integrity": "sha1-dWfb6fL14kZ7x3q4PEopSCQHpdw=",
"dev": true,
"requires": {
"is-extglob": "^2.1.1"
}
},
"is-number": {
"version": "7.0.0",
"resolved": "https://registry.npm.taobao.org/is-number/download/is-number-7.0.0.tgz",
"integrity": "sha1-dTU0W4lnNNX4DE0GxQlVUnoU8Ss=",
"dev": true
},
"json5": { "json5": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npm.taobao.org/json5/download/json5-1.0.1.tgz", "resolved": "https://registry.npm.taobao.org/json5/download/json5-1.0.1.tgz",
@@ -352,6 +441,12 @@
"integrity": "sha1-s1+Pt9FRmQqK69WqUBXAPPcm+EQ=", "integrity": "sha1-s1+Pt9FRmQqK69WqUBXAPPcm+EQ=",
"dev": true "dev": true
}, },
"normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npm.taobao.org/normalize-path/download/normalize-path-3.0.0.tgz",
"integrity": "sha1-Dc1p/yOhybEf0JeDFmRKA4ghamU=",
"dev": true
},
"normalize-wheel": { "normalize-wheel": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npm.taobao.org/normalize-wheel/download/normalize-wheel-1.0.1.tgz", "resolved": "https://registry.npm.taobao.org/normalize-wheel/download/normalize-wheel-1.0.1.tgz",
@@ -363,6 +458,12 @@
"integrity": "sha1-1i27VnlAXXLEc37FhgDp3c8G0kw=", "integrity": "sha1-1i27VnlAXXLEc37FhgDp3c8G0kw=",
"dev": true "dev": true
}, },
"picomatch": {
"version": "2.2.3",
"resolved": "https://registry.npm.taobao.org/picomatch/download/picomatch-2.2.3.tgz",
"integrity": "sha1-RlVH81nMwgbTxI5Goby4m/fuYZ0=",
"dev": true
},
"postcss": { "postcss": {
"version": "8.2.10", "version": "8.2.10",
"resolved": "https://registry.npm.taobao.org/postcss/download/postcss-8.2.10.tgz?cache=0&sync_timestamp=1618159223724&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpostcss%2Fdownload%2Fpostcss-8.2.10.tgz", "resolved": "https://registry.npm.taobao.org/postcss/download/postcss-8.2.10.tgz?cache=0&sync_timestamp=1618159223724&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpostcss%2Fdownload%2Fpostcss-8.2.10.tgz",
@@ -441,6 +542,15 @@
"integrity": "sha1-RD9qIM7WSBor2k+oUypuVdeJoss=", "integrity": "sha1-RD9qIM7WSBor2k+oUypuVdeJoss=",
"dev": true "dev": true
}, },
"readdirp": {
"version": "3.5.0",
"resolved": "https://registry.npm.taobao.org/readdirp/download/readdirp-3.5.0.tgz?cache=0&sync_timestamp=1615717369278&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Freaddirp%2Fdownload%2Freaddirp-3.5.0.tgz",
"integrity": "sha1-m6dMAZsV02UnjS6Ru4xI17TULJ4=",
"dev": true,
"requires": {
"picomatch": "^2.2.1"
}
},
"resize-observer-polyfill": { "resize-observer-polyfill": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npm.taobao.org/resize-observer-polyfill/download/resize-observer-polyfill-1.5.1.tgz", "resolved": "https://registry.npm.taobao.org/resize-observer-polyfill/download/resize-observer-polyfill-1.5.1.tgz",
@@ -465,6 +575,15 @@
"fsevents": "~2.3.1" "fsevents": "~2.3.1"
} }
}, },
"sass": {
"version": "1.32.11",
"resolved": "https://registry.nlark.com/sass/download/sass-1.32.11.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fsass%2Fdownload%2Fsass-1.32.11.tgz",
"integrity": "sha1-sjaz6lXHZgLC7yvQRF8NtYG6ohg=",
"dev": true,
"requires": {
"chokidar": ">=3.0.0 <4.0.0"
}
},
"source-map": { "source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npm.taobao.org/source-map/download/source-map-0.6.1.tgz?cache=0&sync_timestamp=1589682764497&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsource-map%2Fdownload%2Fsource-map-0.6.1.tgz", "resolved": "https://registry.npm.taobao.org/source-map/download/source-map-0.6.1.tgz?cache=0&sync_timestamp=1589682764497&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsource-map%2Fdownload%2Fsource-map-0.6.1.tgz",
@@ -487,6 +606,15 @@
"resolved": "https://registry.npm.taobao.org/to-fast-properties/download/to-fast-properties-2.0.0.tgz", "resolved": "https://registry.npm.taobao.org/to-fast-properties/download/to-fast-properties-2.0.0.tgz",
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4="
}, },
"to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npm.taobao.org/to-regex-range/download/to-regex-range-5.0.1.tgz",
"integrity": "sha1-FkjESq58jZiKMmAY7XL1tN0DkuQ=",
"dev": true,
"requires": {
"is-number": "^7.0.0"
}
},
"util-deprecate": { "util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npm.taobao.org/util-deprecate/download/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npm.taobao.org/util-deprecate/download/util-deprecate-1.0.2.tgz",

View File

@@ -15,6 +15,7 @@
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^1.2.1", "@vitejs/plugin-vue": "^1.2.1",
"@vue/compiler-sfc": "^3.0.5", "@vue/compiler-sfc": "^3.0.5",
"sass": "^1.32.11",
"vite": "^2.1.5" "vite": "^2.1.5"
} }
} }

2
admin/src/main.js vendored
View File

@@ -3,5 +3,5 @@ import App from './App.vue'
import ElementPlus from 'element-plus' import ElementPlus from 'element-plus'
import router from './router/index' import router from './router/index'
import 'element-plus/lib/theme-chalk/index.css' import 'element-plus/lib/theme-chalk/index.css'
// import './styles/common.scss' import './styles/common.scss'
createApp(App).use(ElementPlus).use(router).mount('#app') createApp(App).use(ElementPlus).use(router).mount('#app')

View File

@@ -23,6 +23,11 @@ const router = createRouter({
name: 'user-form', name: 'user-form',
component: () => import('../views/user/form.vue') component: () => import('../views/user/form.vue')
}, },
{
path: '/user-detail',
name: 'user-detail',
component: () => import('../views/user/detail.vue')
},
{ {
path: '/exam', path: '/exam',
name: 'exam', name: 'exam',

View File

@@ -0,0 +1,4 @@
.nexus-table-header {
display: flex;
justify-content: space-between;
}

View File

@@ -26,6 +26,9 @@ const api = {
listUser: (params = {}) => { listUser: (params = {}) => {
return axios.get('user', {params: params}); return axios.get('user', {params: params});
}, },
getUser: (id, params = {}) => {
return axios.get('user/' + id, {params: params});
},
getUserBase: (params = {}) => { getUserBase: (params = {}) => {
return axios.get('user-base', {params: params}); return axios.get('user-base', {params: params});
}, },
@@ -37,7 +40,7 @@ const api = {
return axios.get('exam', {params: params}); return axios.get('exam', {params: params});
}, },
listExamIndex: (params = {}) => { listExamIndex: (params = {}) => {
return axios.get('exam-index', {params: params}); return axios.get('exam-indexes', {params: params});
}, },
storeExam: (params = {}) => { storeExam: (params = {}) => {
return axios.post('exam', params); return axios.post('exam', params);
@@ -52,11 +55,14 @@ const api = {
return axios.delete('exam/' + id); return axios.delete('exam/' + id);
}, },
listClass: (params = {}) => { listClass: (params = {}) => {
return axios.get('class', {params: params}); return axios.get('user-classes', {params: params});
}, },
listExamUser: (params = {}) => { listExamUser: (params = {}) => {
return axios.get('exam-users', {params: params}); return axios.get('exam-users', {params: params});
}, },
deleteExamUser: (id) => {
return axios.delete('exam-users/' + id);
},
} }
export default api export default api

View File

@@ -42,6 +42,7 @@ export const pathMap = {
'agent-allow-form': 'Agent allow form', 'agent-allow-form': 'Agent allow form',
'user': 'User', 'user': 'User',
'user-form': 'User form', 'user-form': 'User form',
'user-detail': 'User detail',
'exam': 'Exam', 'exam': 'Exam',
'exam-form': 'Exam form', 'exam-form': 'Exam form',
'exam-user': 'Exam user', 'exam-user': 'Exam user',

View File

@@ -1,8 +1,13 @@
<template> <template>
<el-card class=""> <el-card class="">
<template #header> <template #header>
<div class="header"> <div class="nexus-table-header">
<el-button type="primary" size="small" icon="el-icon-plus" @click="handleAdd">Add</el-button> <div class="left">
</div>
<div class="right">
<el-button type="primary" size="small" icon="el-icon-plus" @click="handleAdd">Add</el-button>
</div>
</div> </div>
</template> </template>
<el-table <el-table

View File

@@ -57,17 +57,17 @@
label="Action" label="Action"
width="100" width="100"
> >
<!-- <template #default="scope">--> <template #default="scope">
<!-- <a style="cursor: pointer; margin-right: 10px" @click="handleEdit(scope.row.id)">Edit</a>--> <a style="cursor: pointer; margin-right: 10px" @click="handleDetail(scope.row.uid)">Detail</a>
<!-- <el-popconfirm--> <!-- <el-popconfirm-->
<!-- title="Confirm Delete ?"--> <!-- title="Confirm Delete ?"-->
<!-- @confirm="handleDelete(scope.row.id)"--> <!-- @confirm="handleDelete(scope.row.id)"-->
<!-- >--> <!-- >-->
<!-- <template #reference>--> <!-- <template #reference>-->
<!-- <a style="cursor: pointer">Delete</a>--> <!-- <a style="cursor: pointer">Delete</a>-->
<!-- </template>--> <!-- </template>-->
<!-- </el-popconfirm>--> <!-- </el-popconfirm>-->
<!-- </template>--> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<!--总数超过一页再展示分页器--> <!--总数超过一页再展示分页器-->
@@ -130,6 +130,12 @@ export default {
resetTableSort(val, state) resetTableSort(val, state)
fetchTableData() fetchTableData()
} }
const handleDetail = (id) => {
router.push({
name: 'user-detail',
query: {id: id}
})
}
const formatColumnUser = (row, column) => { const formatColumnUser = (row, column) => {
return row.user.username return row.user.username
@@ -148,6 +154,7 @@ export default {
handleAdd, handleAdd,
handleEdit, handleEdit,
handleDelete, handleDelete,
handleDetail,
fetchTableData, fetchTableData,
changePage, changePage,
handleSortChange, handleSortChange,

View File

@@ -0,0 +1,182 @@
<template>
<div class="page-user-detail" v-loading="loading">
<el-card>
<template #header>
<div class="card-header">
<span>Base Info</span>
</div>
</template>
<table class="table-base-info">
<tr>
<th>Field</th>
<th>Value</th>
<th>Actions</th>
<th>Other</th>
</tr>
<tr>
<td>Username</td>
<td>{{baseInfo.username}}</td>
<td></td>
<td colspan="7">
<div class="other-actions">
<el-button type="primary" size="mini">Reset password</el-button>
<el-button type="primary" size="mini">PM</el-button>
<el-button type="primary" size="mini" @click="handleAssignExam">Assign exam</el-button>
</div>
</td>
</tr>
<tr>
<td>Email</td>
<td>{{baseInfo.email}}</td>
<td><el-button size="mini">Change</el-button></td>
</tr>
<tr>
<td>Added</td>
<td>{{baseInfo.added}}</td>
</tr>
<tr>
<td>Class</td>
<td>{{baseInfo.class_text}}</td>
</tr>
<tr>
<td>Uploaded</td>
<td>{{baseInfo.uploaded}}</td>
<td><el-button size="mini">Add</el-button></td>
</tr>
<tr>
<td>Downloaded</td>
<td>{{baseInfo.downloaded}}</td>
<td><el-button size="mini">Add</el-button></td>
</tr>
<tr>
<td>Bonus</td>
<td>{{baseInfo.bonus}}</td>
<td><el-button size="mini">Add</el-button></td>
</tr>
</table>
</el-card>
<el-card v-if="examInfo">
<template #header>
<div class="card-header">
<span>Exam</span>
</div>
</template>
<el-row>
<el-col :span="12">
<table class="table-base-info">
<tr>
<td>Name</td>
<td>{{examInfo.exam && examInfo.exam.name}}</td>
</tr>
<tr>
<td>Created At</td>
<td>{{examInfo.created_at}}</td>
</tr>
<tr>
<td>Exam Time</td>
<td>{{examInfo.begin}} ~ {{examInfo.end}}</td>
</tr>
<tr>
<td>Status</td>
<td>{{examInfo.status_text}}</td>
</tr>
<tr>
<td>Action</td>
<td>
<el-popconfirm
title="Confirm Remove ?"
@confirm="handleRemoveExam(examInfo.id)"
>
<template #reference>
<el-button type="danger" size="small">Remove</el-button>
</template>
</el-popconfirm>
</td>
</tr>
</table>
</el-col>
<el-col :span="12">
<el-table :data="examInfo.progress_formatted">
<el-table-column prop="name" label="Index"></el-table-column>
<el-table-column prop="require_value_formatted" label="Require"></el-table-column>
<el-table-column prop="current_value_formatted" label="Current"></el-table-column>
<el-table-column prop="result" label="Result">
<template #default="scope">
<el-tag v-if="scope.row.passed" type="success">Pass !</el-tag>
<el-tag v-if="!scope.row.passed" type="danger">Not Pass !</el-tag>
</template>
</el-table-column>
</el-table>
</el-col>
</el-row>
</el-card>
</div>
</template>
<script>
import { onMounted, reactive, ref, toRefs } from 'vue'
import { ElMessage } from 'element-plus'
import {useRoute, useRouter} from 'vue-router'
import api from '../../utils/api'
export default {
name: "UserDetail",
setup() {
const route = useRoute()
const router = useRouter()
const { id } = route.query
const state = reactive({
loading: false,
baseInfo: {},
examInfo: {},
matchExams: [],
dialogAssignExamFormVisible: false,
assignExamFormData: {
exam_id: '',
timeRange: []
}
})
onMounted(() => {
fetchPageData()
})
const fetchPageData = async () => {
state.loading = true;
let res = await api.getUser(id)
state.loading = false
state.baseInfo = res.data.base_info
state.examInfo = res.data.exam_info
}
const handleRemoveExam = async (id) => {
let res = await api.deleteExamUser(id)
ElMessage.success(res.msg)
await fetchPageData()
}
const handleAssignExam = async () => {
state.dialogAssignExamFormVisible = true;
}
return {
...toRefs(state),
handleRemoveExam,
handleAssignExam
}
}
}
</script>
<style lang="scss" scoped>
.el-card {
margin-bottom: 20px;
}
.table-base-info {
width: 100%;
text-align: left;
tr {
th {
padding-bottom: 10px;
}
td {
padding: 10px 0;
}
}
}
</style>

View File

@@ -0,0 +1,38 @@
<template>
<el-dialog
title="Assign exam to user"
v-model="dialogAssignExamFormVisible"
center
class="dialog-assign-exam"
>
<el-form :model="assignExamFormData" label-width="150px">
<el-form-item label="Exam">
<el-select v-model="assignExamFormData.exam_id" placeholder="Select an exam...">
<el-option
v-for="item in matchExams"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="Exam time range">
<el-date-picker
v-model="assignExamFormData.timeRange"
type="datetimerange"
format="YYYY-MM-DD HH:mm:ss"
range-separator="to"
start-placeholder="Begin"
end-placeholder="End">
</el-date-picker>
<div class="time-range-help-text">If the time range is not specified, the exam's own configured time range will be used.</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogAssignExamFormVisible = false"> </el-button>
<el-button type="primary" @click="dialogAssignExamFormVisible = false"> </el-button>
</span>
</template>
</el-dialog>
</template>

View File

@@ -77,8 +77,8 @@
label="Action" label="Action"
width="100" width="100"
> >
<!-- <template #default="scope">--> <template #default="scope">
<!-- <a style="cursor: pointer; margin-right: 10px" @click="handleEdit(scope.row.id)">Edit</a>--> <a style="cursor: pointer; margin-right: 10px" @click="handleDetail(scope.row.id)">Detail</a>
<!-- <el-popconfirm--> <!-- <el-popconfirm-->
<!-- title="Confirm Delete ?"--> <!-- title="Confirm Delete ?"-->
<!-- @confirm="handleDelete(scope.row.id)"--> <!-- @confirm="handleDelete(scope.row.id)"-->
@@ -87,7 +87,7 @@
<!-- <a style="cursor: pointer">Delete</a>--> <!-- <a style="cursor: pointer">Delete</a>-->
<!-- </template>--> <!-- </template>-->
<!-- </el-popconfirm>--> <!-- </el-popconfirm>-->
<!-- </template>--> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<!--总数超过一页再展示分页器--> <!--总数超过一页再展示分页器-->
@@ -151,6 +151,13 @@ export default {
fetchTableData() fetchTableData()
} }
const handleDetail = (id) => {
router.push({
name: 'user-detail',
query: {id: id}
})
}
const formatColumnClass = (row, column) => { const formatColumnClass = (row, column) => {
return row.class_text return row.class_text
} }
@@ -168,6 +175,7 @@ export default {
handleAdd, handleAdd,
handleEdit, handleEdit,
handleDelete, handleDelete,
handleDetail,
fetchTableData, fetchTableData,
changePage, changePage,
handleSortChange, handleSortChange,

View File

@@ -44,8 +44,6 @@ class ExamController extends Controller
'name' => 'required|string', 'name' => 'required|string',
'indexes' => 'required|array|min:1', 'indexes' => 'required|array|min:1',
'status' => 'required|in:0,1', 'status' => 'required|in:0,1',
// 'begin' => 'nullable|date',
// 'end' => 'nullable|date',
]; ];
$request->validate($rules); $request->validate($rules);
$result = $this->repository->store($request->all()); $result = $this->repository->store($request->all());
@@ -104,20 +102,4 @@ class ExamController extends Controller
return $this->success($result); return $this->success($result);
} }
public function users(Request $request)
{
$result = $this->repository->listUser($request->all());
$resource = ExamUserResource::collection($result);
return $this->success($resource);
}
public function progress(Request $request)
{
$result = $this->repository->getUserExamProgress(Auth::id());
$resource = new ExamUserResource($result);
return $resource;
// dd($resource->response()->getData(true));
return $this->success($resource);
}
} }

View File

@@ -0,0 +1,87 @@
<?php
namespace App\Http\Controllers;
use App\Http\Resources\ExamResource;
use App\Http\Resources\ExamUserResource;
use App\Http\Resources\UserResource;
use App\Repositories\ExamRepository;
use App\Repositories\UserRepository;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class ExamUserController extends Controller
{
private $repository;
public function __construct(ExamRepository $repository)
{
$this->repository = $repository;
}
/**
* Display a listing of the resource.
*
* @param Request $request
* @return array
*/
public function index(Request $request)
{
$result = $this->repository->listUser($request->all());
$resource = ExamUserResource::collection($result);
return $this->success($resource);
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function store(Request $request)
{
$rules = [
'uid' => 'required',
];
$request->validate($rules);
$result = $this->repository->assignToUser($request->uid, $request->exam_id, $request->begin, $request->end);
$resource = new ExamResource($result);
return $this->success($resource);
}
/**
* Display the specified resource.
*
* @param int $id
* @return array
*/
public function show($id)
{
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return array
*/
public function update(Request $request, $id)
{
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return array
*/
public function destroy($id)
{
$result = $this->repository->removeExamUser($id);
return $this->success($result, 'Remove user exam success!');
}
}

View File

@@ -56,11 +56,12 @@ class UserController extends Controller
* Display the specified resource. * Display the specified resource.
* *
* @param int $id * @param int $id
* @return \Illuminate\Http\Response * @return array
*/ */
public function show($id) public function show($id)
{ {
// $result = $this->repository->getDetail($id);
return $this->success($result);
} }
/** /**
@@ -113,7 +114,7 @@ class UserController extends Controller
return $this->success($resource); return $this->success($resource);
} }
public function exams() public function matchExams()
{ {
$id = Auth::id(); $id = Auth::id();
$examRepository = new ExamRepository(); $examRepository = new ExamRepository();

View File

@@ -25,8 +25,11 @@ class ExamUserResource extends JsonResource
'status_text' => $this->statusText, 'status_text' => $this->statusText,
'created_at' => formatDatetime($this->created_at), 'created_at' => formatDatetime($this->created_at),
'progress' => $this->when($this->progress, $this->progress), 'progress' => $this->when($this->progress, $this->progress),
'progress_formatted' => $this->when($this->progress_formatted, $this->progress_formatted),
'begin' => formatDatetime($this->begin), 'begin' => formatDatetime($this->begin),
'end' => formatDatetime($this->end), 'end' => formatDatetime($this->end),
'uid' => $this->uid,
'exam_id' => $this->exam_id,
'user' => new UserResource($this->whenLoaded('user')), 'user' => new UserResource($this->whenLoaded('user')),
'exam' => new ExamResource($this->whenLoaded('exam')), 'exam' => new ExamResource($this->whenLoaded('exam')),
]; ];

View File

@@ -100,6 +100,11 @@ class User extends Authenticatable
'added' => 'datetime', 'added' => 'datetime',
]; ];
public static $commonFields = [
'id', 'username', 'email', 'class', 'status', 'added', 'avatar',
'uploaded', 'downloaded', 'seedbonus', 'seedtime', 'leechtime'
];
public function checkIsNormal(array $fields = ['status', 'enabled']) public function checkIsNormal(array $fields = ['status', 'enabled'])
{ {
if (in_array('visible', $fields) && $this->getAttribute('status') != self::STATUS_CONFIRMED) { if (in_array('visible', $fields) && $this->getAttribute('status') != self::STATUS_CONFIRMED) {

View File

@@ -9,6 +9,7 @@ use App\Models\Torrent;
use App\Models\User; use App\Models\User;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
class ExamRepository extends BaseRepository class ExamRepository extends BaseRepository
{ {
@@ -150,13 +151,13 @@ class ExamRepository extends BaseRepository
/** /**
* assign exam to user * assign exam to user
* *
* @param $uid * @param int $uid
* @param int $examId * @param null $examId
* @param null $begin * @param null $begin
* @param null $end * @param null $end
* @return mixed * @return mixed
*/ */
public function assignToUser($uid, $examId = 0, $begin = null, $end = null) public function assignToUser(int $uid, $examId = null, $begin = null, $end = null)
{ {
$logPrefix = "uid: $uid, examId: $examId, begin: $begin, end: $end"; $logPrefix = "uid: $uid, examId: $examId, begin: $begin, end: $end";
if ($examId > 0) { if ($examId > 0) {
@@ -247,13 +248,16 @@ class ExamRepository extends BaseRepository
return $newProgress; return $newProgress;
} }
public function getUserExamProgress($uid, $status = null) public function getUserExamProgress($uid, $status = null, $with = ['exam', 'user'])
{ {
$logPrefix = "uid: $uid"; $logPrefix = "uid: $uid";
$query = ExamUser::query()->with(['exam', 'user'])->where('uid', $uid)->orderBy('exam_id', 'desc'); $query = ExamUser::query()->where('uid', $uid)->orderBy('exam_id', 'desc');
if ($status) { if ($status) {
$query->where('status', $status); $query->where('status', $status);
} }
if (!empty($with)) {
$query->with($with);
}
$examUsers = $query->get(); $examUsers = $query->get();
if ($examUsers->isEmpty()) { if ($examUsers->isEmpty()) {
return null; return null;
@@ -262,9 +266,15 @@ class ExamRepository extends BaseRepository
do_log("$logPrefix, user exam more than 1.", 'warning'); do_log("$logPrefix, user exam more than 1.", 'warning');
} }
$examUser = $examUsers->first(); $examUser = $examUsers->first();
$exam = $examUser->exam;
if (empty($examUser->begin) || empty($examUser->end)) {
$examUser->begin = $exam->begin;
$examUser->end = $exam->end;
}
$progress = $this->calculateProgress($examUser); $progress = $this->calculateProgress($examUser);
do_log("$logPrefix, progress: " . nexus_json_encode($progress)); do_log("$logPrefix, progress: " . nexus_json_encode($progress));
$examUser->progress = $progress; $examUser->progress = $progress;
$examUser->progress_formatted = $this->getProgressFormatted($exam, $progress);
return $examUser; return $examUser;
} }
@@ -305,6 +315,49 @@ class ExamRepository extends BaseRepository
} }
private function getProgressFormatted(Exam $exam, array $progress, $locale = null)
{
$result = [];
foreach ($exam->indexes as $key => $index) {
if (!isset($index['checked']) || !$index['checked']) {
continue;
}
$currentValue = $progress[$index['index']] ?? 0;
$requireValue = $index['require_value'];
switch ($index['index']) {
case Exam::INDEX_UPLOADED:
case Exam::INDEX_DOWNLOADED:
$currentValueFormatted = mksize($currentValue);
$requireValueAtomic = $requireValue * 1024 * 1024 * 1024;
break;
case Exam::INDEX_SEED_TIME_AVERAGE:
$currentValueFormatted = mkprettytime($currentValue);
$requireValueAtomic = $requireValue * 3600;
break;
default:
$currentValueFormatted = $currentValue;
$requireValueAtomic = $requireValue;
}
$index['require_value_formatted'] = "$requireValue {$index['unit']}";
$index['current_value'] = $currentValue;
$index['current_value_formatted'] = $currentValueFormatted;
$index['passed'] = $currentValue >= $requireValueAtomic;
$result[] = $index;
}
return $result;
}
public function removeExamUser(int $examUserId)
{
$examUser = ExamUser::query()->findOrFail($examUserId);
$result = DB::transaction(function () use ($examUser) {
$examUser->progresses()->delete();
return $examUser->delete();
});
return $result;
}

View File

@@ -1,6 +1,9 @@
<?php <?php
namespace App\Repositories; namespace App\Repositories;
use App\Http\Resources\ExamUserResource;
use App\Http\Resources\UserResource;
use App\Models\ExamUser;
use App\Models\Setting; use App\Models\Setting;
use App\Models\User; use App\Models\User;
@@ -20,6 +23,27 @@ class UserRepository extends BaseRepository
return $user; return $user;
} }
public function getDetail($id)
{
$user = User::query()->findOrFail($id, User::$commonFields);
$userResource = new UserResource($user);
$baseInfo = $userResource->response()->getData(true)['data'];
$examRep = new ExamRepository();
$examProgress = $examRep->getUserExamProgress($id, ExamUser::STATUS_NORMAL, ['exam']);
if ($examProgress) {
$examResource = new ExamUserResource($examProgress);
$examInfo = $examResource->response()->getData(true)['data'];
} else {
$examInfo = null;
}
return [
'base_info' => $baseInfo,
'exam_info' => $examInfo,
];
}
public function store(array $params) public function store(array $params)
{ {
$password = $params['password']; $password = $params['password'];

View File

@@ -16,15 +16,19 @@ use Illuminate\Support\Facades\Route;
Route::group(['middleware' => ['auth:sanctum']], function () { Route::group(['middleware' => ['auth:sanctum']], function () {
Route::post('logout', [\App\Http\Controllers\AuthenticateController::class, 'logout']); Route::post('logout', [\App\Http\Controllers\AuthenticateController::class, 'logout']);
Route::resource('agent-allow', \App\Http\Controllers\AgentAllowController::class); Route::resource('agent-allow', \App\Http\Controllers\AgentAllowController::class);
Route::resource('user', \App\Http\Controllers\UserController::class); Route::resource('user', \App\Http\Controllers\UserController::class);
Route::get('user-base', [\App\Http\Controllers\UserController::class, 'base']); Route::get('user-base', [\App\Http\Controllers\UserController::class, 'base']);
Route::get('user-exams', [\App\Http\Controllers\UserController::class, 'exams']); Route::get('user-classes', [\App\Http\Controllers\UserController::class, 'classes']);
Route::get('user-match-exams', [\App\Http\Controllers\UserController::class, 'matchExams']);
Route::resource('exam', \App\Http\Controllers\ExamController::class); Route::resource('exam', \App\Http\Controllers\ExamController::class);
Route::get('exam-users', [\App\Http\Controllers\ExamController::class, 'users']); Route::get('exam-indexes', [\App\Http\Controllers\ExamController::class, 'indexes']);
Route::get('exam-index', [\App\Http\Controllers\ExamController::class, 'indexes']);
Route::get('exam-progress', [\App\Http\Controllers\ExamController::class, 'progress']); Route::resource('exam-users', \App\Http\Controllers\ExamUserController::class);
Route::get('class', [\App\Http\Controllers\UserController::class, 'classes']);
}); });
Route::post('login', [\App\Http\Controllers\AuthenticateController::class, 'login']); Route::post('login', [\App\Http\Controllers\AuthenticateController::class, 'login']);