This commit is contained in:
2026-07-21 16:31:07 +08:00
19 changed files with 705 additions and 59 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ module.exports = {
assetsPublicPath: '/', assetsPublicPath: '/',
proxyTable: { proxyTable: {
'/index.php':{ '/index.php':{
target:'https://cogapitest.scszsj.com/', //https://cogapi.scszsj.com/ http://cogapitest.scszsj.com/ http://gg.com/ target:'http://cogapitest.scszsj.com/', //https://cogapi.scszsj.com/ http://cogapitest.scszsj.com/ http://gg.com/
changeOrigin: true, // 如果接口跨域,需要进行这个参数配置 changeOrigin: true, // 如果接口跨域,需要进行这个参数配置
} }
}, },
BIN
View File
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
<!DOCTYPE html><html><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>好游戏计划后台系统</title><link rel=stylesheet type=text/css href=static/css/common.css><link rel=stylesheet type=text/css href=static/css/app.css><script src=./static/UE/ueditor.config.js type=text/javascript charset=utf-8></script><script src=./static/UE/ueditor.all.js type=text/javascript charset=utf-8></script><script src=./static/UE/lang/zh-cn/zh-cn.js type=text/javascript charset=utf-8></script><script src=./static/UE/ueditor.parse.min.js type=text/javascript charset=utf-8></script><link href=/mng/static/css/app.c1444a5e54e1dcbbf95aa3a582e947ae.css rel=stylesheet></head><body><div id=app></div><script type=text/javascript src=/mng/static/js/manifest.34b24ed356662ae59bb9.js></script><script type=text/javascript src=/mng/static/js/vendor.826cc3a26fa7c3b28cbb.js></script><script type=text/javascript src=/mng/static/js/app.8e0860a10b33441a6cf5.js></script></body></html>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["webpack:///webpack/bootstrap 1beccf754d5bd345a4ab"],"names":["parentJsonpFunction","window","chunkIds","moreModules","executeModules","moduleId","chunkId","result","i","resolves","length","installedChunks","push","Object","prototype","hasOwnProperty","call","modules","shift","__webpack_require__","s","installedModules","2","exports","module","l","m","c","d","name","getter","o","defineProperty","configurable","enumerable","get","n","__esModule","object","property","p","oe","err","console","error"],"mappings":"aACA,IAAAA,EAAAC,OAAA,aACAA,OAAA,sBAAAC,EAAAC,EAAAC,GAIA,IADA,IAAAC,EAAAC,EAAAC,EAAAC,EAAA,EAAAC,KACQD,EAAAN,EAAAQ,OAAoBF,IAC5BF,EAAAJ,EAAAM,GACAG,EAAAL,IACAG,EAAAG,KAAAD,EAAAL,GAAA,IAEAK,EAAAL,GAAA,EAEA,IAAAD,KAAAF,EACAU,OAAAC,UAAAC,eAAAC,KAAAb,EAAAE,KACAY,EAAAZ,GAAAF,EAAAE,IAIA,IADAL,KAAAE,EAAAC,EAAAC,GACAK,EAAAC,QACAD,EAAAS,OAAAT,GAEA,GAAAL,EACA,IAAAI,EAAA,EAAYA,EAAAJ,EAAAM,OAA2BF,IACvCD,EAAAY,IAAAC,EAAAhB,EAAAI,IAGA,OAAAD,GAIA,IAAAc,KAGAV,GACAW,EAAA,GAIA,SAAAH,EAAAd,GAGA,GAAAgB,EAAAhB,GACA,OAAAgB,EAAAhB,GAAAkB,QAGA,IAAAC,EAAAH,EAAAhB,IACAG,EAAAH,EACAoB,GAAA,EACAF,YAUA,OANAN,EAAAZ,GAAAW,KAAAQ,EAAAD,QAAAC,IAAAD,QAAAJ,GAGAK,EAAAC,GAAA,EAGAD,EAAAD,QAKAJ,EAAAO,EAAAT,EAGAE,EAAAQ,EAAAN,EAGAF,EAAAS,EAAA,SAAAL,EAAAM,EAAAC,GACAX,EAAAY,EAAAR,EAAAM,IACAhB,OAAAmB,eAAAT,EAAAM,GACAI,cAAA,EACAC,YAAA,EACAC,IAAAL,KAMAX,EAAAiB,EAAA,SAAAZ,GACA,IAAAM,EAAAN,KAAAa,WACA,WAA2B,OAAAb,EAAA,SAC3B,WAAiC,OAAAA,GAEjC,OADAL,EAAAS,EAAAE,EAAA,IAAAA,GACAA,GAIAX,EAAAY,EAAA,SAAAO,EAAAC,GAAsD,OAAA1B,OAAAC,UAAAC,eAAAC,KAAAsB,EAAAC,IAGtDpB,EAAAqB,EAAA,QAGArB,EAAAsB,GAAA,SAAAC,GAA8D,MAApBC,QAAAC,MAAAF,GAAoBA","file":"static/js/manifest.34b24ed356662ae59bb9.js","sourcesContent":[" \t// install a JSONP callback for chunk loading\n \tvar parentJsonpFunction = window[\"webpackJsonp\"];\n \twindow[\"webpackJsonp\"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {\n \t\t// add \"moreModules\" to the modules object,\n \t\t// then flag all \"chunkIds\" as loaded and fire callback\n \t\tvar moduleId, chunkId, i = 0, resolves = [], result;\n \t\tfor(;i < chunkIds.length; i++) {\n \t\t\tchunkId = chunkIds[i];\n \t\t\tif(installedChunks[chunkId]) {\n \t\t\t\tresolves.push(installedChunks[chunkId][0]);\n \t\t\t}\n \t\t\tinstalledChunks[chunkId] = 0;\n \t\t}\n \t\tfor(moduleId in moreModules) {\n \t\t\tif(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {\n \t\t\t\tmodules[moduleId] = moreModules[moduleId];\n \t\t\t}\n \t\t}\n \t\tif(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);\n \t\twhile(resolves.length) {\n \t\t\tresolves.shift()();\n \t\t}\n \t\tif(executeModules) {\n \t\t\tfor(i=0; i < executeModules.length; i++) {\n \t\t\t\tresult = __webpack_require__(__webpack_require__.s = executeModules[i]);\n \t\t\t}\n \t\t}\n \t\treturn result;\n \t};\n\n \t// The module cache\n \tvar installedModules = {};\n\n \t// objects to store loaded and loading chunks\n \tvar installedChunks = {\n \t\t2: 0\n \t};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, {\n \t\t\t\tconfigurable: false,\n \t\t\t\tenumerable: true,\n \t\t\t\tget: getter\n \t\t\t});\n \t\t}\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/mng/\";\n\n \t// on error function for async loading\n \t__webpack_require__.oe = function(err) { console.error(err); throw err; };\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap 1beccf754d5bd345a4ab"],"sourceRoot":""}
+8
View File
@@ -3,6 +3,8 @@
</template> </template>
<script> <script>
import { fetchCsrfToken } from '@/utils/csrf'
export default { export default {
name: 'App', name: 'App',
provide() { provide() {
@@ -15,6 +17,12 @@ export default {
isRouterAlive: true isRouterAlive: true
}; };
}, },
async created() {
try {
await fetchCsrfToken()
} catch (error) {
}
},
methods: { methods: {
reload() { reload() {
this.isRouterAlive = false; this.isRouterAlive = false;
+4
View File
@@ -69,6 +69,10 @@ let GameApi = {
return request._post('/shop/game.applys/index', data, errorback); return request._post('/shop/game.applys/index', data, errorback);
}, },
processApply(data, errorback) {
return request._post('/shop/game.apply/processApply', data, errorback);
},
/*专家选择列表*/ /*专家选择列表*/
storeLists(data, errorback) { storeLists(data, errorback) {
return request._post('/shop/data.store/lists', data, errorback); return request._post('/shop/data.store/lists', data, errorback);
+3 -2
View File
@@ -11,8 +11,9 @@ let IndexApi = {
getCount(data, errorback) { getCount(data, errorback) {
return request._post('/shop/Index/index', data, errorback); return request._post('/shop/Index/index', data, errorback);
}, },
getCaptcha(data, errorback) {
return request._get('/shop/passport/captcha', data, errorback);
},
} }
+283
View File
@@ -0,0 +1,283 @@
# Shop 后台前端 CSRF 防护接入指南
> 适用对象:Shop 后台 Vue.js SPA 前端开发人员
> 关联后端:`app/shop/middleware/CsrfToken.php`、`app/shop/controller/Passport.php`
## 背景
后端已为 shop 后台模块添加 CSRF 防护中间件,规则如下:
- **未登录请求**:直接放行(无认证凭据,无需防护)
- **已登录 + GET/HEAD/OPTIONS**:放行
- **已登录 + POST/PUT/PATCH/DELETE**:必须携带 `X-CSRF-TOKEN` 请求头,且与 session 中的 token 一致,否则返回 `419`
- 登录成功、退出登录会**轮换** session 和 CSRF token
前端必须配合改造,否则登录后所有写操作都会被拦截。
---
## 改造点总览
| 改造点 | 位置 | 作用 |
|--------|------|------|
| ① Token 存储模块 | `src/utils/csrf.js`(新建) | 用内存变量存 token |
| ② axios 请求拦截器 | axios 实例配置 | 写操作自动带 `X-CSRF-TOKEN` |
| ③ axios 响应拦截器 | axios 实例配置 | 处理 419、登录失效 |
| ④ 登录流程 | 登录页 | 登录成功后刷新 token |
| ⑤ 应用初始化 | `App.vue` / `main.js` | 刷新页面后恢复 token |
| ⑥ 退出登录 | 退出按钮处理函数 | 清除本地 token |
---
## ① 封装 Token 存储
token 不要存 `localStorage`/`sessionStorage`(XSS 可读),用内存变量 + 工具函数。
```javascript
// src/utils/csrf.js
import axios from 'axios'
let csrfToken = ''
export function setCsrfToken(token) {
csrfToken = token || ''
}
export function getCsrfToken() {
return csrfToken
}
// 从后端获取 token
export async function fetchCsrfToken() {
const res = await axios.get('/shop/passport/csrfToken')
if (res.data.code === 1 && res.data.data.token) {
setCsrfToken(res.data.data.token)
}
}
```
---
## ② axios 请求拦截器
```javascript
// src/utils/request.js(或现有 axios 实例配置文件)
import axios from 'axios'
import { getCsrfToken } from './csrf'
const request = axios.create({
baseURL: '/',
withCredentials: true, // ★ 必须开启,携带 session cookie
})
// 请求拦截器:写操作自动带上 X-CSRF-TOKEN
request.interceptors.request.use(config => {
const method = (config.method || 'get').toLowerCase()
if (['post', 'put', 'patch', 'delete'].includes(method)) {
const token = getCsrfToken()
if (token) {
config.headers['X-CSRF-TOKEN'] = token
}
}
return config
})
```
**说明**:后端中间件只校验 POST/PUT/PATCH/DELETEGET 不校验,因此只在写操作时加 header。
---
## ③ axios 响应拦截器
```javascript
import { Message } from 'element-ui' // 或 antd / 自定义提示组件
import router from '@/router'
import { setCsrfToken } from './csrf'
request.interceptors.response.use(
response => response.data,
error => {
const status = error.response && error.response.status
// CSRF 校验失败(token 过期或不匹配)
if (status === 419) {
Message.error('登录状态已过期,请重新登录')
setCsrfToken('')
router.replace('/login')
return Promise.reject(error)
}
// 后端自定义的未登录 code(保留原有逻辑)
if (error.response && error.response.data.code === -1) {
router.replace('/login')
return Promise.reject(error)
}
return Promise.reject(error)
}
)
```
---
## ④ 登录流程
后端登录接口需要**图形验证码**,且登录成功后 session 会轮换,**必须重新获取 token**。
```javascript
// src/views/login/index.vue
import { fetchCsrfToken } from '@/utils/csrf'
import request from '@/utils/request'
export default {
data() {
return {
form: { username: '', password: '', captcha: '' },
captchaImg: '',
}
},
async created() {
await this.refreshCaptcha()
},
methods: {
// 获取验证码图(后端 /shop/passport/captcha 返回 base64
async refreshCaptcha() {
const res = await request.get('/shop/passport/captcha')
if (res.code === 1) {
this.captchaImg = res.data.img
}
},
// 提交登录
async handleLogin() {
const res = await request.post('/shop/passport/login', {
username: this.form.username,
password: this.form.password,
captcha: this.form.captcha,
})
if (res.code !== 1) {
// 登录失败,验证码作废,刷新验证码
this.form.captcha = ''
await this.refreshCaptcha()
this.$message.error(res.msg)
return
}
// ★ 关键:登录成功后重新获取 CSRF token
// (登录会重新生成 session,旧 token 失效)
await fetchCsrfToken()
this.$message.success('登录成功')
this.$router.push('/')
},
},
}
```
---
## ⑤ 应用初始化
页面刷新后内存变量丢失,在 App 启动时重新拉取 token:
```javascript
// src/App.vue 或 src/main.js
import { fetchCsrfToken } from '@/utils/csrf'
export default {
async created() {
try {
await fetchCsrfToken()
} catch (e) {
// 网络问题忽略;未登录会话也会返回 token,不会报错
}
},
}
```
**说明**:后端中间件对未登录请求也会生成并返回 token,但真正生效的 token 是**登录后重新生成**的那个。
---
## ⑥ 退出登录
```javascript
import { setCsrfToken } from '@/utils/csrf'
import request from '@/utils/request'
async function handleLogout() {
// logout 是写操作,拦截器会自动带 X-CSRF-TOKEN
await request.post('/shop/passport/logout')
setCsrfToken('')
router.replace('/login')
}
```
---
## 端到端时序图
```
前端 后端
│ │
│ ① 打开登录页 │
│ GET /shop/passport/captcha │
│ ────────────────────────────────────────→│ 返回 base64 验证码图
│ │ (CsrfToken 中间件顺便生成 token)
│ │
│ ② 提交登录 │
│ POST /shop/passport/login │
│ {username, password, captcha} │
│ ────────────────────────────────────────→│ 校验验证码 + 密码
│ │ 登录成功 → Session::regenerate()
│ │ → CsrfToken::regenerateToken()
│ │ ★ session 里现在是新 token
│ │
│ ③ 立即获取新 token │
│ GET /shop/passport/csrfToken │
│ ────────────────────────────────────────→│ 返回 {token: "新token"}
│ │
│ ④ 保存 token 到内存 │
│ csrfToken = "新token" │
│ │
│ ⑤ 后续业务请求(自动带 token) │
│ POST /shop/auth.user/getRoleList │
│ Header: X-CSRF-TOKEN = "新token" │
│ ────────────────────────────────────────→│ CsrfToken 中间件校验通过 ✅
│ │
│ ⑥ token 过期/session 失效 │
│ POST /xxx │
│ ────────────────────────────────────────→│ 返回 419 ❌
│ ←────────────────────────────────────────│
│ 响应拦截器捕获 419 → 跳登录页 │
```
---
## 注意事项
| 项 | 说明 |
|----|------|
| `withCredentials: true` | axios 实例必须配置,否则不带 session cookie |
| 不要存 localStorage | XSS 可读取导致 token 泄露;用内存变量即可(刷新时重新拉取) |
| 验证码一次性 | 登录失败后验证码作废,**必须刷新验证码图** |
| 文件上传也要带 token | `POST /shop/upload/*` 同样校验,拦截器会自动加 header |
| 联调顺序建议 | 先做 ①②(拦截器)→ 再做 ④(登录)→ 最后做 ⑤(初始化) |
| token 生命周期 | 整个 session 有效(默认 8 小时),登录/退出会轮换 |
---
## 联调验证清单
部署后依次验证:
- [ ] 登录页能加载验证码图
- [ ] 输错验证码 → 报错 + 验证码刷新
- [ ] 登录成功 → 接口返回 200,前端拿到新 token
- [ ] 登录后任意 POST 请求 → 200(拦截器自动带 header
- [ ] 手动清除 token 后发 POST → 419 → 自动跳登录页
- [ ] 退出登录 → 跳登录页 → 再登录流程正常
- [ ] 浏览器 DevTools → Application → Cookies → session cookie 显示 `HttpOnly; SameSite=Lax`
+40
View File
@@ -0,0 +1,40 @@
import axios from 'axios'
let csrfToken = ''
let csrfTokenPromise = null
export function setCsrfToken(token) {
csrfToken = token || ''
}
export function getCsrfToken() {
return csrfToken
}
// 登录成功或应用初始化后拉取当前会话的 CSRF token。
export async function fetchCsrfToken(forceRefresh = false) {
if (!forceRefresh && csrfToken) {
return csrfToken
}
if (csrfTokenPromise) {
return csrfTokenPromise
}
csrfTokenPromise = axios.get('/shop/passport/csrfToken')
.then((res) => {
const token = res && res.code === 1 && res.data ? res.data.token : ''
if (!token) {
throw new Error('CSRF Token 获取失败')
}
setCsrfToken(token)
return token
})
.finally(() => {
csrfTokenPromise = null
})
return csrfTokenPromise
}
+41 -10
View File
@@ -1,13 +1,23 @@
import axios from 'axios'; import axios from 'axios';
import { getCsrfToken,setCsrfToken} from '@/utils/csrf';
import qs from 'qs'; import qs from 'qs';
import router from '@/router'; import router from '@/router';
import store from '@/store'; import store from '@/store';
import { import {
Message, Message
Loading
} from 'element-ui'; } from 'element-ui';
import {delCookie,deleteSessionStorage} from '@/utils/base.js'; import {delCookie,deleteSessionStorage} from '@/utils/base.js';
function clearLoginState() {
delCookie('isLogin');
delCookie('baseInfo');
delCookie('userinfo');
setCsrfToken('');
deleteSessionStorage('rolelist');
deleteSessionStorage('authlist');
store.commit('user/setState',{key:'roles',val:null});
}
//axios.defaults.timeout = 5000; //响应时间 //axios.defaults.timeout = 5000; //响应时间
axios.defaults.headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'; //配置请求头 axios.defaults.headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'; //配置请求头
//axios.defaults.baseURL = 'http://www.jjj-shop.com/'; //配置接口地址 //axios.defaults.baseURL = 'http://www.jjj-shop.com/'; //配置接口地址
@@ -18,13 +28,22 @@ axios.defaults.responseType = 'json';
//POST传参序列化(添加请求拦截器) //POST传参序列化(添加请求拦截器)
axios.interceptors.request.use((config) => axios.interceptors.request.use((config) =>
{ {
// console.log(config);
//在发送请求之前做某件事 //在发送请求之前做某件事
if (config.method === 'post' if (config.method === 'post'
&& config.url != '/shop/file.upload/image' && config.url != '/shop/file.upload/image'
&& config.url != '/shop/order.operate/batchDelivery'&& config.url != '/shop/risk.GameData/import') { && config.url != '/shop/order.operate/batchDelivery'&& config.url != '/shop/risk.GameData/import') {
config.data = qs.stringify(config.data); config.data = qs.stringify(config.data);
} }
// 添加 CSRF Token
const method = (config.method || 'get').toLowerCase()
if (['post', 'put', 'patch', 'delete'].includes(method)) {
const token = getCsrfToken()
if (token) {
config.headers = config.headers || {}
config.headers['X-CSRF-TOKEN'] = token
}
}
return config; return config;
}, (error) => }, (error) =>
{ {
@@ -32,6 +51,7 @@ axios.interceptors.request.use((config) =>
return Promise.reject(error); return Promise.reject(error);
}); });
//返回状态判断(添加响应拦截器) //返回状态判断(添加响应拦截器)
axios.interceptors.response.use((res) => axios.interceptors.response.use((res) =>
{ {
@@ -45,13 +65,8 @@ axios.interceptors.response.use((res) =>
}); });
return Promise.reject(res.data); return Promise.reject(res.data);
} else { } else {
delCookie('isLogin'); clearLoginState();
delCookie('baseInfo'); router.replace({
deleteSessionStorage('rolelist');
deleteSessionStorage('authlist');
/*删除stores数据*/
store.commit('user/setState',{key:'roles',val:null});
router.push({
path: '/login', path: '/login',
query: {} query: {}
}); });
@@ -61,6 +76,22 @@ axios.interceptors.response.use((res) =>
return res.data; return res.data;
}, (error) => }, (error) =>
{ {
const status = error.response && error.response.status;
if (status === 419) {
clearLoginState();
Message({
showClose: true,
message: '登录状态已过期,请重新登录',
type: 'error'
});
router.replace({
path: '/login',
query: {}
});
return Promise.reject(error);
}
Message({ Message({
showClose: true, showClose: true,
message: '接口请求异常,请稍后再试~', message: '接口请求异常,请稍后再试~',
+85 -8
View File
@@ -17,10 +17,10 @@
<div class="table-wrap"> <div class="table-wrap">
<el-table size="small" :data="tableData" border style="width: 100%" v-loading="loading"> <el-table size="small" :data="tableData" border style="width: 100%" v-loading="loading">
<!-- <el-table-column prop="id" label="ID" width="80"></el-table-column> --> <!-- <el-table-column prop="id" label="ID" width="80"></el-table-column> -->
<el-table-column prop="name" label="称呼" width="200"></el-table-column> <el-table-column prop="name" label="称呼" width="110"></el-table-column>
<el-table-column prop="phone" label="联系方式" width="200"></el-table-column> <el-table-column prop="phone" label="联系方式" width="120"></el-table-column>
<el-table-column prop="company" label="企业名称" width="250"></el-table-column> <el-table-column prop="company" label="企业名称" width="220"></el-table-column>
<el-table-column prop="company_type" label="企业类型" width="250"></el-table-column> <el-table-column prop="company_type" label="企业类型" width="100"></el-table-column>
<el-table-column prop="need" label="企业需求" width="440"></el-table-column> <el-table-column prop="need" label="企业需求" width="440"></el-table-column>
<!-- <el-table-column prop="status" label="状态" width="120"> <!-- <el-table-column prop="status" label="状态" width="120">
<template slot-scope="scope"> <template slot-scope="scope">
@@ -28,11 +28,27 @@
<span v-if="scope.row.status.value == 0" class="gray">禁用</span> <span v-if="scope.row.status.value == 0" class="gray">禁用</span>
</template> </template>
</el-table-column> --> </el-table-column> -->
<el-table-column prop="satisfaction" label="满意度" width="100">
<template slot-scope="scope">
<span v-if="scope.row.satisfaction === 5">非常满意</span>
<span v-else-if="scope.row.satisfaction === 4">满意</span>
<span v-else-if="scope.row.satisfaction === 3">良好</span>
<span v-else-if="scope.row.satisfaction === 2">不满意</span>
<span v-else-if="scope.row.satisfaction === 1">非常不满意</span>
<span v-else>--</span>
</template>
</el-table-column>
<el-table-column prop="create_time" label="申请时间" width="140"></el-table-column> <el-table-column prop="create_time" label="申请时间" width="140"></el-table-column>
<!-- <el-table-column prop="update_time" label="修改时间" width="140"></el-table-column> --> <el-table-column prop="update_time" label="处理时间" width="140">
<template slot-scope="scope">
<span v-if="scope.row.update_time === scope.row.create_time">--</span>
<span v-else>{{scope.row.update_time}}</span>
</template>
</el-table-column>
<el-table-column label="操作"> <el-table-column label="操作">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button @click="editClick(scope.row)" type="text" size="small" v-auth="'/game/apply/edit'">详情</el-button> <el-button @click="editClick(scope.row)" type="text" size="small" v-auth="'/game/apply/edit'">详情</el-button>
<el-button @click="openProcessDialog(scope.row)" type="text" size="small" v-auth="'/game/apply/edit'" v-if="scope.row.status.value==0">处理</el-button>
<el-button @click="deleteClick(scope.row)" type="text" size="small" v-auth="'/game/apply/delete'">删除</el-button> <el-button @click="deleteClick(scope.row)" type="text" size="small" v-auth="'/game/apply/delete'">删除</el-button>
</template> </template>
</el-table-column> </el-table-column>
@@ -53,12 +69,35 @@
</div> </div>
</div> </div>
<!-- 处理满意度弹窗 -->
<el-dialog
title="处理申请"
:visible.sync="processDialogVisible"
width="400px"
:close-on-click-modal="false"
>
<el-form :model="processForm" label-width="80px">
<el-form-item label="满意度" required>
<el-select v-model="processForm.satisfaction" placeholder="请选择满意度" style="width: 100%;">
<el-option label="非常满意" value="5"></el-option>
<el-option label="满意" value="4"></el-option>
<el-option label="良好" value="3"></el-option>
<el-option label="不满意" value="2"></el-option>
<el-option label="非常不满意" value="1"></el-option>
</el-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="processDialogVisible = false"> </el-button>
<el-button type="primary" :loading="processDialogLoading" @click="processSubmit"> </el-button>
</div>
</el-dialog>
</div> </div>
</template> </template>
<script> <script>
import GameApi from '@/api/game.js'; import GameApi from '@/api/game.js';
import { status } from 'nprogress';
export default { export default {
components: {}, components: {},
inject: ['reload'], inject: ['reload'],
@@ -83,7 +122,14 @@ export default {
/*是否打开编辑弹窗*/ /*是否打开编辑弹窗*/
open_edit: false, open_edit: false,
/*当前编辑的对象*/ /*当前编辑的对象*/
userModel: {} userModel: {},
// 处理申请弹窗
processDialogVisible: false,
processDialogLoading: false,
processForm: {
id: null,
satisfaction: ''
}
}; };
}, },
created() { created() {
@@ -105,7 +151,7 @@ export default {
this.pageSize = val; this.pageSize = val;
this.getTableList(); this.getTableList();
}, },
/*搜索查询*/ /*搜索查询*/
onSubmit() { onSubmit() {
this.curPage = 1; this.curPage = 1;
this.getTableList(); this.getTableList();
@@ -170,6 +216,37 @@ export default {
.catch(error => {}); .catch(error => {});
}) })
.catch(() => {}); .catch(() => {});
},
/* 打开处理申请弹窗 */
openProcessDialog(row) {
this.processForm.id = row.id;
this.processForm.satisfaction = '';
this.processDialogVisible = true;
this.processDialogLoading = false;
},
/* 提交处理申请 */
processSubmit() {
if (!this.processForm.satisfaction) {
this.$message.warning('请选择满意度');
return;
}
this.processDialogLoading = true;
GameApi.processApply({
id: this.processForm.id,
satisfaction: this.processForm.satisfaction,
status: 1
}, true)
.then(() => {
this.$message.success('处理成功');
this.processDialogVisible = false;
this.getTableList();
})
.catch(() => {})
.finally(() => {
this.processDialogLoading = false;
});
} }
} }
}; };
+86 -7
View File
@@ -28,11 +28,27 @@
<span v-if="scope.row.status.value == 0" class="gray">禁用</span> <span v-if="scope.row.status.value == 0" class="gray">禁用</span>
</template> </template>
</el-table-column> --> </el-table-column> -->
<el-table-column prop="satisfaction" label="满意度" width="100">
<template slot-scope="scope">
<span v-if="scope.row.satisfaction === 5">非常满意</span>
<span v-else-if="scope.row.satisfaction === 4">满意</span>
<span v-else-if="scope.row.satisfaction === 3">良好</span>
<span v-else-if="scope.row.satisfaction === 2">不满意</span>
<span v-else-if="scope.row.satisfaction === 1">非常不满意</span>
<span v-else>--</span>
</template>
</el-table-column>
<el-table-column prop="create_time" label="申请时间" width="180"></el-table-column> <el-table-column prop="create_time" label="申请时间" width="180"></el-table-column>
<!-- <el-table-column prop="update_time" label="修改时间" width="140"></el-table-column> --> <el-table-column prop="update_time" label="处理时间" width="140">
<template slot-scope="scope">
<span v-if="scope.row.update_time === scope.row.create_time">--</span>
<span v-else>{{scope.row.update_time}}</span>
</template>
</el-table-column>
<el-table-column label="操作" > <el-table-column label="操作" >
<template slot-scope="scope"> <template slot-scope="scope">
<el-button @click="editClick(scope.row)" type="text" size="small" v-auth="'/game/applys/edit'">详情</el-button> <el-button @click="editClick(scope.row)" type="text" size="small" v-auth="'/game/applys/edit'">详情</el-button>
<el-button @click="openProcessDialog(scope.row)" type="text" size="small" v-auth="'/game/apply/edit'" v-if="scope.row.status.value==0">处理</el-button>
<el-button @click="deleteClick(scope.row)" type="text" size="small" v-auth="'/game/applys/delete'">删除</el-button> <el-button @click="deleteClick(scope.row)" type="text" size="small" v-auth="'/game/applys/delete'">删除</el-button>
</template> </template>
</el-table-column> </el-table-column>
@@ -53,7 +69,29 @@
</div> </div>
</div> </div>
<!-- 处理满意度弹窗 -->
<el-dialog
title="处理申请"
:visible.sync="processDialogVisible"
width="400px"
:close-on-click-modal="false"
>
<el-form :model="processForm" label-width="80px">
<el-form-item label="满意度" required>
<el-select v-model="processForm.satisfaction" placeholder="请选择满意度" style="width: 100%;">
<el-option label="非常满意" :value="5"></el-option>
<el-option label="满意" :value="4"></el-option>
<el-option label="良好" :value="3"></el-option>
<el-option label="不满意" :value="2"></el-option>
<el-option label="非常不满意" :value="1"></el-option>
</el-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="processDialogVisible = false"> </el-button>
<el-button type="primary" :loading="processDialogLoading" @click="processSubmit"> </el-button>
</div>
</el-dialog>
</div> </div>
</template> </template>
@@ -83,7 +121,14 @@ export default {
/*是否打开编辑弹窗*/ /*是否打开编辑弹窗*/
open_edit: false, open_edit: false,
/*当前编辑的对象*/ /*当前编辑的对象*/
userModel: {} userModel: {},
/*处理弹窗相关*/
processDialogVisible: false,
processDialogLoading: false,
processForm: {
satisfaction: null
},
processRowId: null,
}; };
}, },
created() { created() {
@@ -105,7 +150,7 @@ export default {
this.pageSize = val; this.pageSize = val;
this.getTableList(); this.getTableList();
}, },
/*搜索查询*/ /*搜索查询*/
onSubmit() { onSubmit() {
this.curPage = 1; this.curPage = 1;
this.getTableList(); this.getTableList();
@@ -121,7 +166,7 @@ export default {
.then(data => { .then(data => {
self.loading = false; self.loading = false;
self.tableData = data.data.list.data; self.tableData = data.data.list.data;
console.log(self.tableData,222) // console.log(self.tableData, 222)
self.totalDataNumber = data.data.list.total; self.totalDataNumber = data.data.list.total;
}) })
.catch(error => {}); .catch(error => {});
@@ -134,11 +179,9 @@ export default {
/*打开编辑*/ /*打开编辑*/
editClick(row) { editClick(row) {
let self = this;
let params = row.id; let params = row.id;
this.$router.push({ this.$router.push({
path: '/game/applys/edit', path: '/game/applys/edit',
// name: 'mallList',
query: { query: {
id: params id: params
} }
@@ -171,6 +214,42 @@ export default {
.catch(error => {}); .catch(error => {});
}) })
.catch(() => {}); .catch(() => {});
},
/* 打开处理弹窗 */
openProcessDialog(row) {
this.processDialogVisible = true;
this.processDialogLoading = false;
this.processForm.satisfaction = null;
this.processRowId = row.id;
},
/* 确认处理申请 */
processSubmit() {
if (!this.processForm.satisfaction) {
this.$message({
type: 'warning',
message: '请选择满意度'
});
return;
}
this.processDialogLoading = true;
GameApi.processApply({
id: this.processRowId,
satisfaction: this.processForm.satisfaction
}, true)
.then(() => {
this.$message({
type: 'success',
message: '处理成功'
});
this.processDialogVisible = false;
this.getTableList();
})
.catch(() => {})
.finally(() => {
this.processDialogLoading = false;
});
} }
} }
}; };
+89 -25
View File
@@ -21,10 +21,10 @@
<div class="table-wrap"> <div class="table-wrap">
<el-table size="small" :data="tableData" border style="width: 100%" v-loading="loading"> <el-table size="small" :data="tableData" border style="width: 100%" v-loading="loading">
<el-table-column prop="id" label="ID" width="80"></el-table-column> <el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="finance_info.contact_name" label="联系人" width="200"></el-table-column> <el-table-column prop="finance_info.contact_name" label="联系人" width="100"></el-table-column>
<el-table-column prop="finance_info.contact_way" label="联系方式" width="200"></el-table-column> <el-table-column prop="finance_info.contact_way" label="联系方式" width="100"></el-table-column>
<el-table-column prop="company_info.company_name" label="企业名称" width="200"></el-table-column> <el-table-column prop="company_info.company_name" label="企业名称" width="200"></el-table-column>
<el-table-column prop="company_info.company_type" label="企业性质" width="120"> <el-table-column prop="company_info.company_type" label="企业性质" width="100">
<template slot-scope="scope"> <template slot-scope="scope">
<span v-if="scope.row.company_info.company_type == 'develop'">研发</span> <span v-if="scope.row.company_info.company_type == 'develop'">研发</span>
<span v-if="scope.row.company_info.company_type == 'publish'">发行</span> <span v-if="scope.row.company_info.company_type == 'publish'">发行</span>
@@ -41,17 +41,33 @@
<span >{{scope.row.finance_info.fund_amount}}万元</span> <span >{{scope.row.finance_info.fund_amount}}万元</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="status" label="状态" width="120"> <el-table-column prop="status" label="状态" width="100">
<template slot-scope="scope"> <template slot-scope="scope">
<span v-if="scope.row.status == 'processed'" class="green">已处理</span> <span v-if="scope.row.status == 'processed'" class="green">已处理</span>
<span v-if="scope.row.status == 'pending'" class="gray">待处理</span> <span v-if="scope.row.status == 'pending'" class="gray">待处理</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="satisfaction" label="满意度" width="100">
<template slot-scope="scope">
<span v-if="scope.row.satisfaction === '5'">非常满意</span>
<span v-else-if="scope.row.satisfaction === '4'">满意</span>
<span v-else-if="scope.row.satisfaction === '3'">良好</span>
<span v-else-if="scope.row.satisfaction === '2'">不满意</span>
<span v-else-if="scope.row.satisfaction === '1'">非常不满意</span>
<span v-else>--</span>
</template>
</el-table-column>
<el-table-column prop="post_time" label="申请时间" width="140"></el-table-column> <el-table-column prop="post_time" label="申请时间" width="140"></el-table-column>
<el-table-column prop="update_time" label="处理时间" width="140">
<template slot-scope="scope">
<span v-if="scope.row.update_time === scope.row.post_time">--</span>
<span v-else>{{scope.row.update_time}}</span>
</template>
</el-table-column>
<el-table-column label="操作"> <el-table-column label="操作">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button @click="editClick(scope.row)" type="text" size="small" v-auth="'/hyt/apply/detail'">详情</el-button> <el-button @click="editClick(scope.row)" type="text" size="small" v-auth="'/hyt/apply/detail'">详情</el-button>
<el-button @click="processClick(scope.row)" type="text" size="small" v-auth="'/hyt/apply/process'" v-if="scope.row.status=='pending'">处理</el-button> <el-button @click="openProcessDialog(scope.row)" type="text" size="small" v-auth="'/hyt/apply/process'" v-if="scope.row.status=='pending'">处理</el-button>
<el-button @click="deleteClick(scope.row)" type="text" size="small" v-auth="'/hyt/apply/delete'">删除</el-button> <el-button @click="deleteClick(scope.row)" type="text" size="small" v-auth="'/hyt/apply/delete'">删除</el-button>
</template> </template>
</el-table-column> </el-table-column>
@@ -71,6 +87,30 @@
></el-pagination> ></el-pagination>
</div> </div>
</div> </div>
<!-- 处理满意度弹窗 -->
<el-dialog
title="处理申请"
:visible.sync="processDialogVisible"
width="400px"
:close-on-click-modal="false"
>
<el-form :model="processForm" label-width="80px">
<el-form-item label="满意度" required>
<el-select v-model="processForm.satisfaction" placeholder="请选择满意度" style="width: 100%;">
<el-option label="非常满意" value="5"></el-option>
<el-option label="满意" value="4"></el-option>
<el-option label="良好" value="3"></el-option>
<el-option label="不满意" value="2"></el-option>
<el-option label="非常不满意" value="1"></el-option>
</el-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="processDialogVisible = false"> </el-button>
<el-button type="primary" :loading="processDialogLoading" @click="processSubmit"> </el-button>
</div>
</el-dialog>
</div> </div>
</template> </template>
@@ -101,7 +141,14 @@ export default {
/*是否打开编辑弹窗*/ /*是否打开编辑弹窗*/
open_edit: false, open_edit: false,
/*当前编辑的对象*/ /*当前编辑的对象*/
userModel: {} userModel: {},
// 处理申请弹窗数据
processDialogVisible: false,
processDialogLoading: false,
processForm: {
id: null,
satisfaction: ''
}
}; };
}, },
created() { created() {
@@ -168,7 +215,7 @@ export default {
} }
this.tableData = list; this.tableData = list;
console.log(list); // console.log(list);
this.totalDataNumber = total; this.totalDataNumber = total;
this.loading = false; this.loading = false;
}) })
@@ -217,25 +264,42 @@ export default {
.catch(() => {}); .catch(() => {});
}, },
/*删除*/ // 打开处理申请弹窗
processClick(row) { openProcessDialog(row) {
this.$confirm('您确定要处理该申请吗? 是否继续?', '提示', { this.processForm = {
confirmButtonText: '确定', id: row.id,
cancelButtonText: '取消', satisfaction: ''
type: 'warning' };
}) this.processDialogVisible = true;
.then(() => { this.processDialogLoading = false;
hytApi.processApply({ id: row.id }, true) },
.then(data => {
this.$message({ // 处理申请,带满意度
message: '恭喜你,处理成功', processSubmit() {
type: 'success' if (!this.processForm.satisfaction) {
}); this.$message.warning('请选择满意度');
this.getTableList(); return;
}) }
.catch(error => {}); this.processDialogLoading = true;
hytApi.processApply(
{
id: this.processForm.id,
satisfaction: this.processForm.satisfaction
},
true
)
.then(data => {
this.$message({
message: '恭喜你,处理成功',
type: 'success'
});
this.processDialogVisible = false;
this.getTableList();
}) })
.catch(() => {}); .catch(error => {})
.finally(() => {
this.processDialogLoading = false;
});
} }
} }
}; };
+5
View File
@@ -50,6 +50,7 @@
getCookie, getCookie,
deleteSessionStorage deleteSessionStorage
} from '@/utils/base.js'; } from '@/utils/base.js';
import { setCsrfToken } from '@/utils/csrf';
import store from '@/store'; import store from '@/store';
import UpdatePassword from './part/UpdatePassword.vue'; import UpdatePassword from './part/UpdatePassword.vue';
export default { export default {
@@ -115,6 +116,10 @@
delCookie('isLogin'); delCookie('isLogin');
/*删除基本信息*/ /*删除基本信息*/
delCookie('baseInfo'); delCookie('baseInfo');
/*删除个人信息*/
delCookie('userinfo');
/*清空内存中的 csrf token */
setCsrfToken('');
/*删除用户菜单*/ /*删除用户菜单*/
deleteSessionStorage('rolelist'); deleteSessionStorage('rolelist');
/*删除用户权限*/ /*删除用户权限*/
+52 -6
View File
@@ -11,6 +11,26 @@
<el-form-item prop="account"><el-input type="text" v-model="ruleForm.account" auto-complete="off" placeholder="账号"></el-input></el-form-item> <el-form-item prop="account"><el-input type="text" v-model="ruleForm.account" auto-complete="off" placeholder="账号"></el-input></el-form-item>
<!--密码--> <!--密码-->
<el-form-item prop="checkPass"><el-input type="password" v-model="ruleForm.checkPass" auto-complete="off" placeholder="密码"></el-input></el-form-item> <el-form-item prop="checkPass"><el-input type="password" v-model="ruleForm.checkPass" auto-complete="off" placeholder="密码"></el-input></el-form-item>
<!-- 图形验证码 -->
<el-form-item prop="captcha">
<div style="display: flex; align-items: center;">
<el-input
type="text"
v-model="ruleForm.captcha"
auto-complete="off"
placeholder="验证码"
style="flex: 1;"
></el-input>
<el-image
v-if="code_img"
:src="code_img"
style="width: 100px; height: 40px; margin-left: 10px; cursor: pointer; flex-shrink: 0;"
@click="getCaptcha"
alt="点击刷新验证码"
title="点击刷新验证码"
></el-image>
</div>
</el-form-item>
<!--登录--> <!--登录-->
<el-form-item><el-button type="primary" style="width:100%;" @click.native.prevent="SubmitFunc" :loading="logining">登录</el-button></el-form-item> <el-form-item><el-button type="primary" style="width:100%;" @click.native.prevent="SubmitFunc" :loading="logining">登录</el-button></el-form-item>
</el-form> </el-form>
@@ -22,6 +42,7 @@ import IndexApi from '@/api/index.js';
// import bgimg from '@/assets/img/login_bg.jpg'; // import bgimg from '@/assets/img/login_bg.jpg';
import UserApi from '@/api/user.js'; import UserApi from '@/api/user.js';
import { setCookie, delCookie, setSessionStorage } from '@/utils/base.js'; import { setCookie, delCookie, setSessionStorage } from '@/utils/base.js';
import { fetchCsrfToken, setCsrfToken } from '@/utils/csrf.js';
export default { export default {
data() { data() {
return { return {
@@ -36,14 +57,17 @@ export default {
/*用户名*/ /*用户名*/
account: '', account: '',
/*密码*/ /*密码*/
checkPass: '' checkPass: '',
captcha:''
}, },
code_img:'',
/*验证规则*/ /*验证规则*/
rules: { rules: {
/*用户名*/ /*用户名*/
account: [{ required: true, message: '请输入用户名', trigger: 'blur' }], account: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
/*密码*/ /*密码*/
checkPass: [{ required: true, message: '请输入密码', trigger: 'blur' }] checkPass: [{ required: true, message: '请输入密码', trigger: 'blur' }],
captcha: [{ required: true, message: '请输入验证码', trigger: 'blur' }]
}, },
/*基础配置*/ /*基础配置*/
baseData: {} baseData: {}
@@ -51,6 +75,14 @@ export default {
}, },
created() { created() {
//this.getData(); //this.getData();
//清除缓存
delCookie('baseInfo');
/*保存个人信息*/
delCookie('userinfo');
/*设置一个登录状态*/
delCookie('isLogin');
setCsrfToken('');
this.getCaptcha();
if(this.$route.query.from&&this.$route.query.from=='admin'){ if(this.$route.query.from&&this.$route.query.from=='admin'){
this.saasLogin(); this.saasLogin();
} }
@@ -69,6 +101,15 @@ export default {
self.loading = false; self.loading = false;
}); });
}, },
getCaptcha() {
IndexApi.getCaptcha(true)
.then(res => {
console.log(res,222222);
this.code_img = res.data.img;
})
.catch(error => {});
},
/*登录方法*/ /*登录方法*/
SubmitFunc(ev) { SubmitFunc(ev) {
@@ -78,19 +119,22 @@ export default {
this.logining = true; this.logining = true;
var Params = { var Params = {
username: this.ruleForm.account, username: this.ruleForm.account,
password: this.ruleForm.checkPass password: this.ruleForm.checkPass,
captcha: this.ruleForm.captcha
}; };
/*调用登录接口*/ /*调用登录接口*/
UserApi.login(Params, true) UserApi.login(Params, true)
.then(res => { .then(async res => {
this.logining = false; this.logining = false;
if (res.code == 1) { if (res.code == 1) {
/*保存个人信息*/ /*保存个人信息*/
setCookie('userinfo', res.data); setCookie('userinfo', res.data);
/*设置一个登录状态*/ /*设置一个登录状态*/
setCookie('isLogin', true); setCookie('isLogin', true);
await fetchCsrfToken(true);
/*跳转到首页*/ /*跳转到首页*/
this.$router.push({ path: '/' }); this.$router.push({ path: '/' });
} else { } else {
this.$message({ this.$message({
message: '登录失败', message: '登录失败',
@@ -101,6 +145,8 @@ export default {
.catch(error => { .catch(error => {
//接口调用方法统一处理 //接口调用方法统一处理
this.logining = false; this.logining = false;
this.ruleForm.captcha = '';
this.getCaptcha();
}); });
} }
}); });
@@ -108,14 +154,14 @@ export default {
saasLogin(){ saasLogin(){
/*调用登录接口*/ /*调用登录接口*/
UserApi.saasLogin({}, true) UserApi.saasLogin({}, true)
.then(res => { .then(async res => {
console.log(res);
if (res.code == 1) { if (res.code == 1) {
delCookie('baseInfo'); delCookie('baseInfo');
/*保存个人信息*/ /*保存个人信息*/
setCookie('userinfo', res.data); setCookie('userinfo', res.data);
/*设置一个登录状态*/ /*设置一个登录状态*/
setCookie('isLogin', true); setCookie('isLogin', true);
await fetchCsrfToken(true);
/*跳转到首页*/ /*跳转到首页*/
this.$router.push({ path: '/' }); this.$router.push({ path: '/' });
} else { } else {