提交 5ac0f5dd authored 作者: matian's avatar matian

feat:新增fdc登录

上级 ca0ba122
...@@ -13,6 +13,7 @@ module.exports = { ...@@ -13,6 +13,7 @@ module.exports = {
'vue/setup-compiler-macros': true 'vue/setup-compiler-macros': true
}, },
rules: { rules: {
'@typescript-eslint/no-explicit-any': 'off',
'vue/multi-word-component-names': 'off' 'vue/multi-word-component-names': 'off'
} }
} }
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
"axios": "^0.26.1", "axios": "^0.26.1",
"element-plus": "^2.1.10", "element-plus": "^2.1.10",
"pinia": "^2.0.13", "pinia": "^2.0.13",
"qs": "^6.10.3",
"swiper": "^8.0.7", "swiper": "^8.0.7",
"vue": "^3.2.31", "vue": "^3.2.31",
"vue-router": "^4.0.14" "vue-router": "^4.0.14"
...@@ -24,6 +25,7 @@ ...@@ -24,6 +25,7 @@
"devDependencies": { "devDependencies": {
"@rushstack/eslint-patch": "^1.1.1", "@rushstack/eslint-patch": "^1.1.1",
"@types/node": "^17.0.23", "@types/node": "^17.0.23",
"@types/qs": "^6.9.7",
"@vitejs/plugin-vue": "^2.3.1", "@vitejs/plugin-vue": "^2.3.1",
"@vue/eslint-config-typescript": "^10.0.0", "@vue/eslint-config-typescript": "^10.0.0",
"@vue/tsconfig": "^0.1.3", "@vue/tsconfig": "^0.1.3",
......
...@@ -3,6 +3,7 @@ lockfileVersion: 5.3 ...@@ -3,6 +3,7 @@ lockfileVersion: 5.3
specifiers: specifiers:
'@rushstack/eslint-patch': ^1.1.1 '@rushstack/eslint-patch': ^1.1.1
'@types/node': ^17.0.23 '@types/node': ^17.0.23
'@types/qs': ^6.9.7
'@vitejs/plugin-vue': ^2.3.1 '@vitejs/plugin-vue': ^2.3.1
'@vue/eslint-config-typescript': ^10.0.0 '@vue/eslint-config-typescript': ^10.0.0
'@vue/tsconfig': ^0.1.3 '@vue/tsconfig': ^0.1.3
...@@ -14,6 +15,7 @@ specifiers: ...@@ -14,6 +15,7 @@ specifiers:
eslint: ^8.12.0 eslint: ^8.12.0
eslint-plugin-vue: ^8.6.0 eslint-plugin-vue: ^8.6.0
pinia: ^2.0.13 pinia: ^2.0.13
qs: ^6.10.3
sass: ^1.50.0 sass: ^1.50.0
swiper: ^8.0.7 swiper: ^8.0.7
typescript: ~4.6.3 typescript: ~4.6.3
...@@ -29,6 +31,7 @@ dependencies: ...@@ -29,6 +31,7 @@ dependencies:
axios: 0.26.1 axios: 0.26.1
element-plus: 2.1.10_vue@3.2.31 element-plus: 2.1.10_vue@3.2.31
pinia: 2.0.13_typescript@4.6.3+vue@3.2.31 pinia: 2.0.13_typescript@4.6.3+vue@3.2.31
qs: 6.10.3
swiper: 8.0.7 swiper: 8.0.7
vue: 3.2.31 vue: 3.2.31
vue-router: 4.0.14_vue@3.2.31 vue-router: 4.0.14_vue@3.2.31
...@@ -36,6 +39,7 @@ dependencies: ...@@ -36,6 +39,7 @@ dependencies:
devDependencies: devDependencies:
'@rushstack/eslint-patch': 1.1.1 '@rushstack/eslint-patch': 1.1.1
'@types/node': 17.0.23 '@types/node': 17.0.23
'@types/qs': 6.9.7
'@vitejs/plugin-vue': 2.3.1_vite@2.9.1+vue@3.2.31 '@vitejs/plugin-vue': 2.3.1_vite@2.9.1+vue@3.2.31
'@vue/eslint-config-typescript': 10.0.0_324c71289bd52a05964d00266adebb09 '@vue/eslint-config-typescript': 10.0.0_324c71289bd52a05964d00266adebb09
'@vue/tsconfig': 0.1.3_@types+node@17.0.23 '@vue/tsconfig': 0.1.3_@types+node@17.0.23
...@@ -197,6 +201,10 @@ packages: ...@@ -197,6 +201,10 @@ packages:
resolution: {integrity: sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==} resolution: {integrity: sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==}
dev: true dev: true
/@types/qs/6.9.7:
resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==}
dev: true
/@typescript-eslint/eslint-plugin/5.18.0_a07dca3bdfc4bfa60f4dda0c1f9e3287: /@typescript-eslint/eslint-plugin/5.18.0_a07dca3bdfc4bfa60f4dda0c1f9e3287:
resolution: {integrity: sha512-tzrmdGMJI/uii9/V6lurMo4/o+dMTKDH82LkNjhJ3adCW22YQydoRs5MwTiqxGF9CSYxPxQ7EYb4jLNlIs+E+A==} resolution: {integrity: sha512-tzrmdGMJI/uii9/V6lurMo4/o+dMTKDH82LkNjhJ3adCW22YQydoRs5MwTiqxGF9CSYxPxQ7EYb4jLNlIs+E+A==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
...@@ -731,7 +739,6 @@ packages: ...@@ -731,7 +739,6 @@ packages:
dependencies: dependencies:
function-bind: 1.1.1 function-bind: 1.1.1
get-intrinsic: 1.1.1 get-intrinsic: 1.1.1
dev: true
/callsites/3.1.0: /callsites/3.1.0:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
...@@ -1437,7 +1444,6 @@ packages: ...@@ -1437,7 +1444,6 @@ packages:
/function-bind/1.1.1: /function-bind/1.1.1:
resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
dev: true
/functional-red-black-tree/1.0.1: /functional-red-black-tree/1.0.1:
resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==}
...@@ -1449,7 +1455,6 @@ packages: ...@@ -1449,7 +1455,6 @@ packages:
function-bind: 1.1.1 function-bind: 1.1.1
has: 1.0.3 has: 1.0.3
has-symbols: 1.0.3 has-symbols: 1.0.3
dev: true
/get-ready/1.0.0: /get-ready/1.0.0:
resolution: {integrity: sha512-mFXCZPJIlcYcth+N8267+mghfYN9h3EhsDa6JSnbA3Wrhh/XFpuowviFcsDeYZtKspQyWyJqfs4O6P8CHeTwzw==} resolution: {integrity: sha512-mFXCZPJIlcYcth+N8267+mghfYN9h3EhsDa6JSnbA3Wrhh/XFpuowviFcsDeYZtKspQyWyJqfs4O6P8CHeTwzw==}
...@@ -1530,7 +1535,6 @@ packages: ...@@ -1530,7 +1535,6 @@ packages:
/has-symbols/1.0.3: /has-symbols/1.0.3:
resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
dev: true
/has-tostringtag/1.0.0: /has-tostringtag/1.0.0:
resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==}
...@@ -1544,7 +1548,6 @@ packages: ...@@ -1544,7 +1548,6 @@ packages:
engines: {node: '>= 0.4.0'} engines: {node: '>= 0.4.0'}
dependencies: dependencies:
function-bind: 1.1.1 function-bind: 1.1.1
dev: true
/http-errors/2.0.0: /http-errors/2.0.0:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
...@@ -1934,7 +1937,6 @@ packages: ...@@ -1934,7 +1937,6 @@ packages:
/object-inspect/1.12.0: /object-inspect/1.12.0:
resolution: {integrity: sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==} resolution: {integrity: sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==}
dev: true
/once/1.4.0: /once/1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
...@@ -2149,7 +2151,6 @@ packages: ...@@ -2149,7 +2151,6 @@ packages:
engines: {node: '>=0.6'} engines: {node: '>=0.6'}
dependencies: dependencies:
side-channel: 1.0.4 side-channel: 1.0.4
dev: true
/queue-microtask/1.2.3: /queue-microtask/1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
...@@ -2308,7 +2309,6 @@ packages: ...@@ -2308,7 +2309,6 @@ packages:
call-bind: 1.0.2 call-bind: 1.0.2
get-intrinsic: 1.1.1 get-intrinsic: 1.1.1
object-inspect: 1.12.0 object-inspect: 1.12.0
dev: true
/slash/3.0.0: /slash/3.0.0:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
......
...@@ -45,7 +45,7 @@ function genClassName(menu: IMenu) { ...@@ -45,7 +45,7 @@ function genClassName(menu: IMenu) {
.app-container { .app-container {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
max-width: 1200px; // max-width: 1200px;
// margin: 56px auto 72px; // margin: 56px auto 72px;
margin: 0 auto; margin: 0 auto;
} }
......
...@@ -77,7 +77,7 @@ ...@@ -77,7 +77,7 @@
background-color: #aa1941; background-color: #aa1941;
} }
.app-footer-inner { .app-footer-inner {
max-width: 1200px; // max-width: 1200px;
margin: 0 auto; margin: 0 auto;
padding: 50px 0; padding: 50px 0;
display: flex; display: flex;
...@@ -156,7 +156,7 @@ ...@@ -156,7 +156,7 @@
background-color: #fff; background-color: #fff;
} }
.app-copyright-inner { .app-copyright-inner {
max-width: 1200px; // max-width: 1200px;
margin: 0 auto; margin: 0 auto;
font-size: 12px; font-size: 12px;
line-height: 40px; line-height: 40px;
......
...@@ -19,7 +19,9 @@ const logout = () => { ...@@ -19,7 +19,9 @@ const logout = () => {
<header class="app-header"> <header class="app-header">
<div class="app-header-left"> <div class="app-header-left">
<div class="logo"> <div class="logo">
<router-link to="/"><img src="https://zws-imgs-pub.ezijing.com/pc/base/ezijing-logo-white.svg" /></router-link> <router-link to="/"
><img src="https://webapp-pub.ezijing.com/project_online/fdc/fdc_header_logo.png"
/></router-link>
</div> </div>
</div> </div>
<div class="app-header-right"> <div class="app-header-right">
...@@ -50,15 +52,16 @@ const logout = () => { ...@@ -50,15 +52,16 @@ const logout = () => {
<style lang="scss"> <style lang="scss">
.app-header { .app-header {
position: sticky; position: absolute;
top: 0; top: 44px;
left: 72px;
z-index: 1000; z-index: 1000;
padding: 0 20px; padding: 0 20px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
height: 64px; height: 64px;
background-color: #aa1941; background-color: #fff;
color: #fff; color: #fff;
.logo { .logo {
width: 120px; width: 120px;
......
...@@ -4,7 +4,7 @@ export default { name: 'AppLayout' } ...@@ -4,7 +4,7 @@ export default { name: 'AppLayout' }
<script setup lang="ts"> <script setup lang="ts">
import AppHeader from './Header.vue' import AppHeader from './Header.vue'
import AppAside from './Aside.vue' // import AppAside from './Aside.vue'
import AppMain from './Main.vue' import AppMain from './Main.vue'
withDefaults(defineProps<{ sidebar?: boolean; hasTitle?: boolean }>(), { withDefaults(defineProps<{ sidebar?: boolean; hasTitle?: boolean }>(), {
sidebar: true, sidebar: true,
...@@ -15,22 +15,23 @@ withDefaults(defineProps<{ sidebar?: boolean; hasTitle?: boolean }>(), { ...@@ -15,22 +15,23 @@ withDefaults(defineProps<{ sidebar?: boolean; hasTitle?: boolean }>(), {
<template> <template>
<div class="app-layout"> <div class="app-layout">
<app-header :hasTitle="hasTitle"></app-header> <app-header :hasTitle="hasTitle"></app-header>
<div class="app-layout-container"> <!-- <div class="app-layout-container"> -->
<app-aside v-if="sidebar"></app-aside> <!-- <app-aside v-if="sidebar"></app-aside> -->
<app-main></app-main> <app-main></app-main>
</div> <!-- </div> -->
</div> </div>
</template> </template>
<style lang="scss"> <style lang="scss">
.app-layout { .app-layout {
width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 100vh; // min-height: 100vh;
background-color: #ededed; background: rgba(0, 0, 0, 0);
}
.app-layout-container {
flex: 1;
display: flex;
} }
// .app-layout-container {
// flex: 1;
// display: flex;
// }
</style> </style>
...@@ -12,8 +12,7 @@ ...@@ -12,8 +12,7 @@
.app-main { .app-main {
position: relative; position: relative;
flex: 1; flex: 1;
padding: 20px; top: 166px;
overflow: hidden;
} }
.app-main-inner { .app-main-inner {
margin: 0 auto; margin: 0 auto;
......
...@@ -27,7 +27,7 @@ const navList = [ ...@@ -27,7 +27,7 @@ const navList = [
border-top: 1px solid #ebebeb; border-top: 1px solid #ebebeb;
} }
.app-nav-inner { .app-nav-inner {
max-width: 1200px; // max-width: 1200px;
margin: 0 auto; margin: 0 auto;
} }
.app-menu { .app-menu {
......
import httpRequest from '@/utils/axios'
// 查询证书
export function getCard(params: { personal_name: string; id_number: string; certificate_number: string }) {
return httpRequest.get('https://learn-api.ezijing.com/api/lms-financial/v1/education/search-certificate', { params })
}
// 发送验证码
export function sendCode(data: { account: string }) {
return httpRequest.post('/api/usercenter/user/send-code', data)
}
// 登录
export function login(data: { account: string; password: string }) {
return httpRequest.post('/api/passport/rest/login', data)
}
<script setup lang="ts">
import { reactive } from 'vue'
import { useRouter } from 'vue-router'
import * as api from '../api'
const $router = useRouter()
const form = reactive({
name: '', // 姓名
idNumber: '', // 身份证号
cardNumber: '' // 证书编号
})
const queryResultList = reactive({ list: [] as any[] })
const handleSubmit = () => {
const params = {
personal_name: form.name, // 姓名
id_number: form.idNumber, // 身份证号
certificate_number: form.cardNumber // 证书编号
}
api.getCard(params).then(res => {
queryResultList.list = res.data
window.localStorage.setItem('list', JSON.stringify(queryResultList.list))
console.log(res, 'pppp')
$router.push({
path: 'detail'
})
})
console.log('submit!')
}
</script>
<template>
<el-form :model="form">
<el-form-item>
<el-input v-model="form.name" placeholder="姓名">
<template #prefix>
<el-icon class="el-input_icon_name"></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-input v-model="form.idNumber" placeholder="身份证号">
<template #prefix>
<el-icon class="el-input_icon_id"></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-input v-model="form.cardNumber" placeholder="证书编号">
<template #prefix>
<el-icon class="el-input_icon_card"></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item>
<div class="btn_query" @click="handleSubmit">查询</div>
</el-form-item>
</el-form>
</template>
<style scoped lang="scss">
.btn_query {
width: 292px;
height: 40px;
background: #01379e;
opacity: 1;
border-radius: 20px;
text-align: center;
line-height: 40px;
font-size: 16px;
font-weight: 400;
color: #ffffff;
}
.el-input_icon_card {
display: block;
background: url('https://webapp-pub.ezijing.com/project_online/fdc/icon_card.png') center no-repeat;
background-size: 100% 100%;
height: 12.8px;
margin-left: 21px;
}
.el-input_icon_id {
display: block;
background: url('https://webapp-pub.oss-cn-beijing.aliyuncs.com/project_online/fdc/icon_idNum.png') center no-repeat;
background-size: 100% 100%;
height: 13.4px;
margin-left: 21px;
}
.el-input_icon_name {
display: block;
background: url('https://webapp-pub.oss-cn-beijing.aliyuncs.com/project_online/fdc/icon_name.png') center no-repeat;
background-size: 100% 100%;
height: 13.7px;
margin-left: 21px;
}
::v-deep {
.el-input__inner {
margin-left: 9px;
}
.el-input {
--el-input-border-radius: 20px;
height: 40px;
}
}
</style>
<template>
<div class="course">
<dl>
<dt><img src="https://webapp-pub.ezijing.com/project/vst/icon_zyjs_01.png" /></dt>
<dd>
<h4>职业名称</h4>
<p>互联网营销师</p>
</dd>
</dl>
<dl>
<dt><img src="https://webapp-pub.ezijing.com/project/vst/icon_zyjs_02.png" /></dt>
<dd>
<h4>职业编码</h4>
<p>4-01-02-07</p>
</dd>
</dl>
<dl>
<dt><img src="https://webapp-pub.ezijing.com/project/vst/icon_zyjs_03.png" /></dt>
<dd>
<h4>认证背景</h4>
<p>
2020年人社部发布9个新职业中,互联网营销师到2025年需求将达到约4500万人,人才缺口约在4000万人左右。互联网营销师职业的设立,有利于健全从业人员的素质化教育和行业规范化的建设,进一步满足净化网络商业环境的需求。
</p>
</dd>
</dl>
<dl>
<dt><img src="https://webapp-pub.ezijing.com/project/vst/icon_zyjs_04.png" /></dt>
<dd>
<h4>职业定义</h4>
<p>在数字化信息平台上,运用网络的交互性与传播公信力,对企业产品进行营销推广的人员。</p>
</dd>
</dl>
<dl>
<dt><img src="https://webapp-pub.ezijing.com/project/vst/icon_zyjs_05.png" /></dt>
<dd>
<h4>职业能力特征</h4>
<p>
具有较强的学习、理解、分析、计算及判断能力,具有一定的空间感,色觉正常、语言表达正常,手指、手臂灵活,形体动作协调性好。
</p>
</dd>
</dl>
<dl>
<dt><img src="https://webapp-pub.ezijing.com/project/vst/icon_zyjs_06.png" /></dt>
<dd>
<h4>主要工作内容</h4>
<p>
1、研究数字化平台的用户定位和运营方式<br />2、接受企业委托,对企业资质和产品质量等信息进行审核<br />
3、选定相关产品,设计策划营销方案,制定佣金结算方式<br />
4、搭建数字化营销场景,通过直播或短视频等形式对产品进行多平台营销推广<br />
5、提升自身传播影响力,加强用户群体活跃度,促进产品从关注到购买的转化率<br />6、签订销售订单,结算销售货款<br />
7、负责协调产品的售后服务<br />8、采集分析销售数据,对企业或产品提出优化性建议
</p>
</dd>
</dl>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import * as api from '../api'
const form = reactive({
phone: '', // 手机号
password: '' // 密码
})
const isShowGetCode = ref(false)
const isDisposed = ref(false)
const time = ref(60)
const loginType = ref('')
const isCode = ref(false)
// 登录
const handleLogin = () => {
if (isCode.value === true) {
loginType.value = '2'
} else {
loginType.value = '1'
}
const params = {
account: form.phone,
password: form.password,
type: loginType.value,
service: window.location.origin
}
api.login(params).then(res => {
console.log(res, '0000')
window.location.href = 'https://fdc-admin.ezijing.com'
})
}
// 获取验证码
const handleGetCode = () => {
isShowGetCode.value = !isShowGetCode.value
}
function getCode() {
if (form.phone === '') {
ElMessage('请输入手机号')
return
}
const params = {
account: form.phone
}
api.sendCode(params).then(res => {
console.log('获取验证码结果', res)
isDisposed.value = true
isCode.value = true
handleTimeChange()
})
}
// 倒计时
const handleTimeChange = () => {
if (time.value <= 0) {
isDisposed.value = false
time.value = 60
} else {
setTimeout(() => {
time.value--
handleTimeChange()
}, 1000)
}
}
</script>
<template>
<el-form :model="form">
<el-form-item>
<el-input v-model="form.phone" placeholder="手机号">
<template #prefix>
<el-icon class="el-input_icon_name"></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item v-if="isShowGetCode === false">
<el-input v-model="form.password" placeholder="密码">
<template #prefix>
<el-icon class="el-input_icon_password"></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item v-if="isShowGetCode === true">
<el-input v-model="form.password" placeholder="验证码">
<template #prefix>
<el-icon class="el-input_icon_password"></el-icon>
</template>
<template #append>
<el-button :disabled="isDisposed" @click="getCode">{{
isDisposed ? `${time}s后重新获取` : '获取验证码'
}}</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item>
<div class="btn_code" @click="handleGetCode">{{ isShowGetCode === false ? '验证码' : '密码登录' }}</div>
</el-form-item>
<el-form-item>
<div class="btn_query" @click="handleLogin">登录</div>
</el-form-item>
</el-form>
</template>
<style scoped lang="scss">
.btn_query {
width: 292px;
height: 40px;
background: #01379e;
opacity: 1;
border-radius: 20px;
text-align: center;
line-height: 40px;
font-size: 16px;
font-weight: 400;
color: #ffffff;
}
.btn_code {
font-size: 14px;
font-weight: 400;
color: #01379e;
padding-left: 237px;
}
.el-input_icon_name {
display: block;
background: url('https://webapp-pub.ezijing.com/project_online/fdc/icon_name.png') center no-repeat;
background-size: 100% 100%;
height: 13.7px;
margin-left: 21px;
}
.el-input_icon_password {
display: block;
background: url('https://webapp-pub.ezijing.com/project_online/fdc/icon_pwd.png') center no-repeat;
background-size: 100% 100%;
height: 15px;
margin-left: 21px;
}
::v-deep {
.el-input__inner {
margin-left: 9px;
}
.el-input {
--el-input-border-radius: 20px;
height: 40px;
}
.el-input-group--append > .el-input__wrapper {
height: 40px;
}
}
</style>
<script lang="ts" setup>
const list = [
{ desc: '选择待报考认证的项目' },
{ desc: '在考试培训单位报名,参加考前培训' },
{ desc: '完成课程学习后,开具培训课时证明' },
{ desc: '提交培训证明并重新填写报名表' },
{ desc: '等待分配考场' },
{ desc: '考前1-2周打印准考证' },
{ desc: '参加线下试卷考试或机考' },
{ desc: '考试成绩于考试结束后3-4周出成绩。' }
]
</script>
<template>
<div class="process">
<dl v-for="(item, index) in list" :key="index">
<dt>{{ index + 1 }}</dt>
<dd>{{ item.desc }}</dd>
</dl>
</div>
</template>
<template>
<article class="article">
<p>
为贯彻落实党中央、国务院重大决策部署和《“十四五”数字经济发展规划》的有关要求,加强数字经济领域产业人员的能力培训,紫荆教育充分发挥自身优势开展数字经济领域的职业技能培训,探索人才评价的新模式,优化培训方式及培训流程,并为此专门成立了数字职业技能培训中心。
</p>
<p>
数字职业技能培训中心联合院校共同开展多元化人才培养模式,构建“专业培养+技能实训+学历提升的职业培训体系,培养高层次数字化技能型人才。与此同时,紫荆教育与企业进行了深度融合,共同搭建职业技能提升平台,提供线上线下一体化的解决方案,培养高素质的数字经济职业技能人才,助力企业实现人才的数字化转型。
</p>
</article>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import * as api from '../api'
const form = reactive({
phone: '', // 手机号
password: '' // 密码
})
const isShowGetCode = ref(false)
const isDisposed = ref(false)
const time = ref(60)
const loginType = ref('')
const isCode = ref(false)
// 登录
const handleLogin = () => {
if (isCode.value === true) {
loginType.value = '2'
} else {
loginType.value = '1'
}
const params = {
account: form.phone,
password: form.password,
type: loginType.value,
service: window.location.origin
}
api.login(params).then(res => {
console.log(res, '0000')
window.location.href = 'https://fdc-learning.ezijing.com'
})
}
// 获取验证码
const handleGetCode = () => {
isShowGetCode.value = !isShowGetCode.value
}
function getCode() {
if (form.phone === '') {
ElMessage('请输入手机号')
return
}
const params = {
account: form.phone
}
api.sendCode(params).then(res => {
console.log('获取验证码结果', res)
isDisposed.value = true
isCode.value = true
handleTimeChange()
})
}
// 倒计时
const handleTimeChange = () => {
if (time.value <= 0) {
isDisposed.value = false
time.value = 60
} else {
setTimeout(() => {
time.value--
handleTimeChange()
}, 1000)
}
}
</script>
<template>
<el-form :model="form">
<el-form-item>
<el-input v-model="form.phone" placeholder="手机号">
<template #prefix>
<el-icon class="el-input_icon_name"></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item v-if="isShowGetCode === false">
<el-input v-model="form.password" placeholder="密码">
<template #prefix>
<el-icon class="el-input_icon_password"></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item v-if="isShowGetCode === true">
<el-input v-model="form.password" placeholder="验证码">
<template #prefix>
<el-icon class="el-input_icon_password"></el-icon>
</template>
<template #append>
<el-button :disabled="isDisposed" @click="getCode">{{
isDisposed ? `${time}s后重新获取` : '获取验证码'
}}</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item>
<div class="btn_code" @click="handleGetCode">{{ isShowGetCode === false ? '验证码' : '密码登录' }}</div>
</el-form-item>
<el-form-item>
<div class="btn_query" @click="handleLogin">登录</div>
</el-form-item>
</el-form>
</template>
<style scoped lang="scss">
.btn_query {
width: 292px;
height: 40px;
background: #01379e;
opacity: 1;
border-radius: 20px;
text-align: center;
line-height: 40px;
font-size: 16px;
font-weight: 400;
color: #ffffff;
}
.btn_code {
font-size: 14px;
font-weight: 400;
color: #01379e;
padding-left: 237px;
}
.el-input_icon_name {
display: block;
background: url('https://webapp-pub.ezijing.com/project_online/fdc/icon_name.png') center no-repeat;
background-size: 100% 100%;
height: 13.7px;
margin-left: 21px;
}
.el-input_icon_password {
display: block;
background: url('https://webapp-pub.ezijing.com/project_online/fdc/icon_pwd.png') center no-repeat;
background-size: 100% 100%;
height: 15px;
margin-left: 21px;
}
::v-deep {
.el-input__inner {
margin-left: 9px;
}
.el-input {
--el-input-border-radius: 20px;
height: 40px;
}
.el-input-group--append > .el-input__wrapper {
height: 40px;
}
}
</style>
...@@ -8,9 +8,7 @@ export const routes: Array<RouteRecordRaw> = [ ...@@ -8,9 +8,7 @@ export const routes: Array<RouteRecordRaw> = [
redirect: '/internet/index', redirect: '/internet/index',
children: [ children: [
{ path: 'index', component: () => import('./views/Index.vue') }, { path: 'index', component: () => import('./views/Index.vue') },
{ path: 'course', component: () => import('./views/Course.vue') }, { path: 'detail', component: () => import('./views/QueryDetail.vue') }
{ path: 'outline', component: () => import('./views/Outline.vue') },
{ path: 'apply', component: () => import('./views/Apply.vue') }
] ]
} }
] ]
<!-- 申请条件 -->
<script lang="ts" setup>
import menus from '../menus'
</script>
<template>
<AppContainer :menus="menus">
<template #banner><img src="https://webapp-pub.ezijing.com/project/vst/banner_zy.png" /></template>
<template #inner>
<div class="course">
<dl>
<dt><img src="https://webapp-pub.ezijing.com/project/vst/icon_apply.png" /></dt>
<dd>
<h4>符合以下条件之一,可申报五级/初级工</h4>
<p>
1、累计从事本职业或相关职业1 年(含)以上。<br />
2、经本职业或相关职业五级/初级工正规培训达规定标准学时,并取得结业证。<br />
3、本职业或相关职业学徒期满。
</p>
</dd>
</dl>
<dl>
<dt><img src="https://webapp-pub.ezijing.com/project/vst/icon_apply.png" /></dt>
<dd>
<h4>符合以下条件之一,可申报符合四级/中级工</h4>
<p>
1、取得本职业或相关职业五级/初级工职业资格证书(技能等级证书)后,累计从事本职业或相关职业工作 3
年(含)以上。<br />
2、累计从事本职业或相关职业工作 4 年(含)以上。<br />
3、累计从事本职业或相关职业工作 2
年(含)以上,经本职业四级/中级工正规培训达规定标准学时,并取得结业证书。<br />
4、取得技工学校本专业或相关专业毕业证书(含尚未取得毕业证书的在校应届毕业生);或取得经评估论证、以中级技能为培养目标的中等及以上职业学校本专业或相关专业毕业证书(含尚未取得毕业证书的在校应届毕业生)。
</p>
</dd>
</dl>
<dl>
<dt><img src="https://webapp-pub.ezijing.com/project/vst/icon_apply.png" /></dt>
<dd>
<h4>符合以下条件之一,可申报三级/高级工</h4>
<p>
1、取得本职业或相关职业四级/中级工职业资格证书(技能等级证书)后,累计从事本职业或相关职业工作 3
年(含)以上,经本职业三级/高级工正规培训达规定标准学时,并取得结业证书。<br />
2、取得本职业或相关职业四级/中级工职业资格证书(技能等级证书)后,累计从事本职业或相关职业工作 4
年(含)以上。<br />
3、取得本职业或相关职业四级/中级工职业资格证书(技能等级证书),并具有高级技工学校、技师学院毕业证书(含尚未取得毕业证书的在校应届毕业生);或取得本职业或相关职业四级/中级工职业资格证书(技能等级证书),并具有经评估论证、以高级技能为培养目标的高等职业学校本专业或相关专业毕业证书(含尚未取得毕业证书的在校应届毕业生)。<br />
4、具有大专及以上本专业或相关专业毕业证书,并取得本职业或相关职业四级/中级工职业资格证书(技能等级证书)后,累计从事本职业或相关职业工作
2 年(含)以上。
</p>
</dd>
</dl>
<dl>
<dt><img src="https://webapp-pub.ezijing.com/project/vst/icon_apply.png" /></dt>
<dd>
<h4>符合以下条件之一,可申报技师:</h4>
<p>
1、取得本职业或相关职业三级/高级工职业资格证书(技能等级证书)后,累计从事本职业或相关职业工作 3
年(含)以上,经本职业二级/技师正规培训达规定标准学时,并取得结业证书。<br />
2、取得本职业三级/高级工职业资格证书(技能等级证书)后,累计从事本职业或相关职业工作 4 年(含)以上。<br />
3、取得本职业或相关职业三级/高级工职业资格证书(技能等级证书)的高级技工学校、技师学院毕业生,累计从事本职业或相关职业工作
3 年(含)以上;或取得本职业或相关职业预备技师证书的技师学院毕业生,累计从事本职业或相关职业工作 2
年(含)以上。
</p>
</dd>
</dl>
<dl>
<dt><img src="https://webapp-pub.ezijing.com/project/vst/icon_apply.png" /></dt>
<dd>
<h4>符合以下条件之一,可申报一级/高级技师:</h4>
<p>
1、取得本职业或相关职业二级/技师职业资格证书(技能等级证书)后,累计从事本职业或相关职业工作 3
年(含)以上,经本职业一级/高级技师正规培训达规定标准学时,并取得结业证书。<br />
2、取得本职业或相关职业二级/技师职业资格证书(技能等级证书)后,累计从事本职业或相关职业工作 4
年(含)以上。
</p>
</dd>
</dl>
</div>
</template>
</AppContainer>
</template>
<style lang="scss">
@import '@/assets/css/channel.scss';
</style>
<!-- 课程介绍 -->
<script lang="ts" setup>
import menus from '../menus'
</script>
<template>
<AppContainer :menus="menus">
<template #banner><img src="https://webapp-pub.ezijing.com/project/vst/banner_zy.png" /></template>
<template #inner>
<div class="course">
<dl>
<dt><img src="https://webapp-pub.ezijing.com/project/vst/icon_kcjs_01.png" /></dt>
<dd>
<h4>课程概述</h4>
<p>
本课程共计38学时,任何有志于从事该职业,或对此职业感兴趣的人员均为此课程的适用学习对象。主要学习内容包括计算机网络基础知识、互联网传播基础知识、数字化营销知识、互联网营销基础知识、安全基础知识和相关法律法规、产品策划、直播销售、粉丝运营和技术服务。
</p>
</dd>
</dl>
<dl>
<dt><img src="https://webapp-pub.ezijing.com/project/vst/icon_kcjs_02.png" /></dt>
<dd>
<h4>课程目标</h4>
<p>了解互联网营销师的基本知识、运营方法及相关法律法规,掌握实用的营销手段、技能。</p>
</dd>
</dl>
<dl>
<dt><img src="https://webapp-pub.ezijing.com/project/vst/icon_kcjs_03.png" /></dt>
<dd>
<h4>考核评价</h4>
<p>理论考试,主要考核从业人员从事本职业应掌握的基本要求和相关知识要求。</p>
</dd>
</dl>
<dl>
<dt><img src="https://webapp-pub.ezijing.com/project/vst/icon_kcjs_04.png" /></dt>
<dd>
<h4>学习对象</h4>
<p>任何有志于从事该职业或对此职业感兴趣的人员均为此课程的适用学习对象。</p>
</dd>
</dl>
<dl>
<dt><img src="https://webapp-pub.ezijing.com/project/vst/icon_kcjs_05.png" /></dt>
<dd>
<h4>学习内容</h4>
<p>该职业从业者应具备的相关知识和技能,内容包括通用知识、专业基础知识和专业技能知识三大部分。</p>
</dd>
</dl>
</div>
</template>
</AppContainer>
</template>
<style lang="scss">
@import '@/assets/css/channel.scss';
</style>
<!-- 职业介绍 --> <script setup lang="ts">
<script lang="ts" setup> import { ref } from 'vue'
import Job from '../components/Job.vue'
import Project from '../components/Project.vue'
import Process from '../components/Process.vue'
</script>
import CardQuery from '../components/CardQuery.vue'
import StudyLogin from '../components/StudyLogin.vue'
import ManagerLogin from '../components/ManagerLogin.vue'
const tabList: Array<{ tabName: string }> = [
{
tabName: '证书查询'
},
{
tabName: '学习登录'
},
{
tabName: '管理员登录'
}
]
const activeIndex = ref(0)
function handleTabChange(index: number) {
activeIndex.value = index
}
</script>
<template> <template>
<AppContainer> <div class="home">
<template #banner><img src="https://webapp-pub.ezijing.com/project/vst/banner_zy.png" /></template> <div class="home_box">
<template #inner> <div class="home_box_tab">
<el-tabs class="my-tabs"> <div class="tab_item" v-for="(item, index) in tabList" :key="index" @click="handleTabChange(index)">
<el-tab-pane label="职业介绍"> <div class="tab_item_name" :class="activeIndex === index ? 'active_name' : 'inactive_name'">
<Job /> {{ item.tabName }}
</el-tab-pane> </div>
<el-tab-pane label="项目介绍"> </div>
<Project /> </div>
</el-tab-pane> <div class="home_box_form" v-if="activeIndex === 0">
<el-tab-pane label="报名流程"> <CardQuery />
<Process /> </div>
</el-tab-pane> <div v-if="activeIndex === 1">
</el-tabs> <StudyLogin />
</template> </div>
</AppContainer> <div v-if="activeIndex === 2">
<ManagerLogin />
</div>
<div class="home_box_tips" v-if="activeIndex === 0">请输入姓名、身份证号、证书编号中任意两项信息进行查询。</div>
<div class="home_box_tips" v-if="activeIndex === 1">请输入手机号、验证码或密码登录在线学习系统。</div>
<div class="home_box_line"></div>
<div class="home_box_bottom">
<div>报名咨询:林老师</div>
<div>电话:(86)13910136972</div>
</div>
</div>
</div>
</template> </template>
<style lang="scss"> <style lang="scss">
@import '@/assets/css/channel.scss'; .home {
height: 734px;
width: 100%;
background: url(https://webapp-pub.oss-cn-beijing.aliyuncs.com/project_online/fdc/%20fdc_bg.png) no-repeat;
background-size: 100% 100%;
position: relative;
.home_box {
display: flex;
justify-content: space-around;
align-items: center;
flex-direction: column;
width: 472px;
height: 496px;
background: #ffffff;
opacity: 1;
border-radius: 20px;
position: absolute;
top: 151px;
left: 50%;
transform: translateX(-50%);
.home_box_tab {
display: flex;
justify-content: space-around;
width: 410px;
background: rgba(255, 255, 255, 0.39);
border: 1px solid #e5e5e5;
opacity: 1;
border-radius: 22px;
position: absolute;
top: 53px;
left: 50%;
transform: translateX(-50%);
.tab_item_name {
width: 136px;
height: 40px;
opacity: 1;
border-radius: 22px;
line-height: 40px;
text-align: center;
font-size: 18px;
font-weight: bold;
}
.active_name {
color: #ffffff;
background: #01379e;
}
.inactive_name {
background: #ffffff;
color: #01379e;
}
}
.home_box_form {
position: absolute;
top: 146px;
left: 50%;
transform: translateX(-50%);
width: 292px;
}
.home_box_tips {
font-size: 12px;
font-weight: 400;
line-height: 18px;
color: #333333;
position: absolute;
top: 402px;
left: 50%;
transform: translateX(-50%);
white-space: nowrap;
}
.home_box_line {
width: 410px;
height: 0px;
border: 1px solid #e3e3e3;
opacity: 1;
position: absolute;
bottom: 66px;
left: 50%;
transform: translateX(-50%);
}
.home_box_bottom {
position: absolute;
bottom: 21px;
left: 50%;
transform: translateX(-50%);
font-size: 12px;
font-weight: 400;
line-height: 1.5;
color: #bababa;
text-align: center;
}
}
}
</style> </style>
<!-- 课程大纲 -->
<script lang="ts" setup>
import menus from '../menus'
const list = [
{ name: '计算机网络基础知识', quantity: '9节' },
{ name: '互联网传播基础知识', quantity: '5节' },
{ name: '数字化营销基础知识', quantity: '11节' },
{ name: '互联网营销基础知识', quantity: '11节' },
{ name: '安全基础知识和相关法律法规', quantity: '2节' },
{ name: '产品策划', quantity: '17节' },
{ name: '直播销售', quantity: '17节' },
{ name: '粉丝运营', quantity: '9节' },
{ name: '技术服务', quantity: '4节' }
]
</script>
<template>
<AppContainer :menus="menus">
<template #banner><img src="https://webapp-pub.ezijing.com/project/vst/banner_zy.png" /></template>
<template #inner>
<div class="outline">
<table>
<tr>
<th class="outline-name">课程名称</th>
<th class="outline-quantity">课节量</th>
</tr>
<tr v-for="(item, index) in list" :key="index">
<td>{{ item.name }}</td>
<td>{{ item.quantity }}</td>
</tr>
</table>
</div>
</template>
</AppContainer>
</template>
<style lang="scss">
@import '@/assets/css/channel.scss';
</style>
<script setup lang="ts">
import { reactive, onMounted } from 'vue'
const queryResultList = reactive({ list: [] as any[] })
onMounted(() => {
queryResultList.list = JSON.parse(window.localStorage.getItem('list') || '0')
})
</script>
<template>
<div class="main_content">
<div class="home-box" v-if="queryResultList.list.length > 0">
<div v-for="(item, index) in queryResultList.list" :key="index" class="home-box-content">
<div class="home-box-content-btn">
<a :href="item.url" :download="item.url" target="_blank"><el-button round class="btn">证书下载</el-button></a>
</div>
<div class="home-box-content-list">
<embed data-v-2e21b479="" width="100%" height="100%" :src="item.url" class="embed" />
</div>
</div>
</div>
<div class="home-box-else" v-else>
<!-- 暂无信息 -->
<el-result title="暂无信息" icon="info" class="el-result"> </el-result>
</div>
</div>
</template>
<style scoped lang="scss">
.home-box {
width: 1200px;
height: 100vh;
margin: auto;
.home-box-content {
display: flex;
justify-content: center;
align-items: flex-end;
flex-direction: column;
padding: 20px 0 20px 0;
.home-box-content-btn {
align-self: flex-end;
.btn {
background: #a81a42;
color: #fff;
width: 150px;
height: 50px;
}
}
.home-box-content-list {
width: 100%;
height: 500px;
margin-top: 20px;
}
}
}
.home-box-else {
width: 1200px;
height: calc(100vh - 300px);
margin: auto;
text-align: center;
position: relative;
.el-result {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
</style>
import axios from 'axios' import axios from 'axios'
import qs from 'qs'
import { ElMessage } from 'element-plus'
const httpRequest = axios.create({ const httpRequest = axios.create({
baseURL: 'https://learn-api.ezijing.com', baseURL: 'https://learn-api.ezijing.com',
timeout: 60000, timeout: 60000,
withCredentials: true withCredentials: true,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}) })
// 请求拦截
httpRequest.interceptors.request.use(
function (config) {
if (config.headers?.['Content-Type'] === 'application/x-www-form-urlencoded') {
config.data = qs.stringify(config.data)
}
if (config.headers?.['Content-Type'] === 'multipart/form-data') {
const formData = new window.FormData()
for (const key in config.data) {
formData.append(key, config.data[key])
}
config.data = formData
}
return config
},
function (error) {
return Promise.reject(error)
}
)
// 响应拦截 // 响应拦截
httpRequest.interceptors.response.use( httpRequest.interceptors.response.use(
function (response) { function (response) {
const { data } = response const { data } = response
// 正常返回 console.log(data, 'ooooooo')
if (data.code === 0) {
return data
}
// 未登录 // 未登录
if (data.code === 4001) { if (data.code === 4001) {
location.href = `${import.meta.env.VITE_LOGIN_URL}?rd=${encodeURIComponent(location.href)}` location.href = `${import.meta.env.VITE_LOGIN_URL}?rd=${encodeURIComponent(location.href)}`
return Promise.reject(data)
} }
return Promise.reject(data) if (data.code === 1) {
ElMessage.error(data.message || data.msg)
return Promise.reject(data)
}
return data
}, },
function (error) { function (error) {
if (error.response) { if (error.response) {
...@@ -27,6 +53,8 @@ httpRequest.interceptors.response.use( ...@@ -27,6 +53,8 @@ httpRequest.interceptors.response.use(
if (status === 403) { if (status === 403) {
location.href = `${import.meta.env.VITE_LOGIN_URL}?rd=${encodeURIComponent(location.href)}` location.href = `${import.meta.env.VITE_LOGIN_URL}?rd=${encodeURIComponent(location.href)}`
} else { } else {
ElMessage.error(message)
console.error(`${status}: ${message}`) console.error(`${status}: ${message}`)
} }
} else { } else {
......
...@@ -19,6 +19,7 @@ export default defineConfig(({ mode }) => { ...@@ -19,6 +19,7 @@ export default defineConfig(({ mode }) => {
eslintrc: { enabled: true } eslintrc: { enabled: true }
}) })
], ],
server: { server: {
open: true, open: true,
host: 'dev.ezijing.com', host: 'dev.ezijing.com',
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论