refactor: API调用重构

environments/test/deployments/1
向文可 3 years ago
parent c785dd6479
commit cd3350bd2c

@ -0,0 +1,2 @@
VITE_BASE_URL=https://gateway.mashibing.cn
VITE_REQUEST_TIMEOUT=20000

@ -0,0 +1,4 @@
include:
- project: 'yunwei/cicd/xuexipingtai/entrypoint'
ref: main
file: '/main.yml'

@ -163,12 +163,15 @@ proxy.$copy('hello world');
| copy | 复制文本 |
| download | 下载指定地址文件 |
| excel | 导出 excel 文件并下载 |
| dict | 字典查询 |
| name | 昵称和姓名拼接显示 |
## 全局组件
| 名称 | 功能介绍 |
| --------- | ------------ |
| ------------- | ------------ |
| ElEditor | 富文本编辑器 |
| ElUploadImage | 图片上传 |
| TableList | 通用列表组件 |
## 心得总结

@ -1,3 +1,9 @@
# 待解决
## 多个路由引用同一个 vue 页面时,打开这几个路由后就会有多个 TAB 页,切换 TAB 页时因为这几个路由引用的其实是同一个组件,所以初始数据会保持一致。比如新增页面应该是没有数据的,编辑数据需要回显,但是先打开新增页面再打开编辑页面,新增页面就会同步为编辑页面。
# 踩坑日记
## perttier 保存时不会自动格式化属性排序、需要执行命令才能格式化
> xwk 2022.3.23
@ -20,6 +26,12 @@
> xwk 0328
> el-tabs 使用 label 插槽,应该是 ElementPlus 的 BUG[Github 已有 Issues](https://github.com/element-plus/element-plus/issues/6839)
> ElementPlus 已在 2.1.7 版本修复了这个 BUG
## Uncaught (in promise) TypeError: Cannot read properties of null (reading 'shapeFlag')
> xwk 0330
> 对 props 使用对象展开符可能会展开 null
## 偶现 SVG 图标颜色渲染异常

@ -9,7 +9,7 @@
"scripts": {
"dev": "vite",
"build:test": "vite build --mode test",
"build:preview": "vite build --mode preview",
"build:beta": "vite build --mode beta",
"build:prod": "vite build --mode prod",
"preview": "vite preview",
"prepare": "husky install",
@ -20,14 +20,15 @@
"@vueup/vue-quill": "^1.0.0-beta.8",
"axios": "^0.26.1",
"dayjs": "^1.11.0",
"element-plus": "^2.1.2",
"element-plus": "^2.1.7",
"lodash": "^4.17.21",
"qs": "^6.10.3",
"quill-image-uploader": "^1.2.2",
"sortablejs": "^1.14.0",
"vue": "^3.2.25",
"vue-router": "^4.0.14",
"vuex": "^4.0.2"
"vuex": "^4.0.2",
"vuex-persistedstate": "^4.1.0"
},
"devDependencies": {
"@commitlint/cli": "^13.2.1",

@ -11,7 +11,7 @@ export function sendSmsCode(params) {
// 登录
export async function login(data) {
return request({
url: '/uaa/sso/appManageLogin',
url: '/user/loginByPasswordToB',
method: 'post',
data,
});
@ -19,7 +19,7 @@ export async function login(data) {
// 获取用户信息
export function getUserInfo() {
return request({
url: '/uc/user/v1/info/token',
url: '/user/current',
method: 'get',
});
}

@ -1,59 +1,29 @@
const mock = (data) =>
new Promise((resolve) => {
setTimeout(() => {
resolve(data);
}, Math.random() * 1500 + 500);
import request from '@/utils/request.js';
export const search = (params) => {
return request({
url: '/employee',
method: 'get',
params,
});
let list = [
{
id: 1,
username: 'user001',
nickname: '张三',
sex: 1,
avatar: 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fup.enterdesk.com%2Fedpic%2Ffd%2Ff1%2Fda%2Ffdf1dacb8ff0b8f13ed29bcbee42f328.jpeg&refer=http%3A%2F%2Fup.enterdesk.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1650201540&t=ba213738d8f11e79302fab71602856f2',
loginTime: Date.now(),
enabled: true,
},
{
id: 2,
username: 'user003',
nickname: '李四',
sex: 0,
avatar: 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fup.enterdesk.com%2Fedpic%2Ffd%2Ff1%2Fda%2Ffdf1dacb8ff0b8f13ed29bcbee42f328.jpeg&refer=http%3A%2F%2Fup.enterdesk.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1650201540&t=ba213738d8f11e79302fab71602856f2',
loginTime: Date.now(),
enabled: true,
},
{
id: 3,
username: 'user003',
nickname: '王五',
sex: 1,
avatar: 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fup.enterdesk.com%2Fedpic%2Ffd%2Ff1%2Fda%2Ffdf1dacb8ff0b8f13ed29bcbee42f328.jpeg&refer=http%3A%2F%2Fup.enterdesk.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1650201540&t=ba213738d8f11e79302fab71602856f2',
loginTime: Date.now(),
enabled: false,
},
];
export const findUserList = (data) => {
console.info(data);
return mock({ content: list, totalElements: list.length });
};
export const createUser = (data) => {
data = {
id: new Date().getTime(),
...data,
};
list.push(data);
return mock(data);
};
export const updateUser = (data) => {
let old = list.find((item) => item.id === data.id);
Object.assign(old, data);
return mock(old);
export const create = (data) => {
return request({
url: '/employee',
method: 'post',
data,
});
};
export const removeUser = (ids) => {
list = list.filter((item) => !ids.includes(item.id));
return mock(true);
export const update = (data) => {
return request({
url: '/employee',
method: 'put',
data,
});
};
export const getUserDetail = (id) => {
return mock(list.find((item) => item.id === id));
export const remove = (params) => {
return request({
url: '/employee',
method: 'delete',
params,
});
};

@ -0,0 +1,147 @@
<template>
<div class="upload-box">
<el-upload
v-bind="props"
:before-upload="handleBeforeUpload"
:file-list="imgList"
:headers="headers"
list-type="picture-card"
:on-exceed="handleExceed"
:on-preview="handlePreview"
>
<el-icon name="Plus" />
</el-upload>
<el-image
v-if="preview"
ref="refsPreview"
alt="图片预览"
:src="preview"
style="position: absolute; z-index: -9999"
@close="preview = null"
@load="$event.path[0].click()"
/>
<div class="el-upload__tip">支持小于 {{ fmtSize }} 文件</div>
</div>
</template>
<script setup lang="jsx">
import config from '@/configs';
import { ElMessage } from '@/plugins/element-plus';
import 'element-plus/es/components/image/style/css';
const store = useStore();
const props = defineProps({
action: {
type: String,
default: config.baseURL + '/edu-oss/oss/fileUpload',
},
data: {
type: Object,
default() {
return { service: 'msb-edu-course' };
},
},
headers: {
type: Object,
default() {
return {};
},
},
drag: {
type: Boolean,
default: false,
},
multiple: {
type: Boolean,
defualt: false,
},
disabled: {
type: Boolean,
defualt: false,
},
limit: {
type: Number,
default: 1,
},
size: {
type: Number,
default: 1024 * 1024 * 20,
},
accept: {
type: String,
default: 'image/*',
},
});
const emits = defineEmits(['update:modelValue']);
let headers = { ...props.headers };
headers['Authorization'] = store.state.local.token;
const imgList = ref([]);
const attrs = useAttrs();
watch(
() => attrs.modelValue,
(value) => {
if (
unref(imgList)
.map((item) => item.url)
.join(',') !== value.join(',')
) {
imgList.value = (value instanceof Array ? value : [value])
.filter((item) => item)
.map((item) => {
return {
name: item,
response: {
data: item,
},
url: item,
};
});
}
},
{ immediate: true, deep: true }
);
watch(
() => imgList,
() => {
const arr = unref(imgList).map((item) => item.response?.data);
if (arr.every((item) => !!item)) {
const value = props.limit === 1 ? arr[0] : arr;
emits('update:modelValue', value);
}
},
{ deep: true }
);
const refsPreview = ref(null);
const preview = ref('');
const handlePreview = (file) => {
console.info('[upload] preview', file);
preview.value = file.url;
};
const handleExceed = (list) => {
console.info('[upload] exceed', list);
ElMessage.error('超出最大上传数量');
};
const handleBeforeUpload = (file) => {
console.info('[upload] upload', file);
let res = true;
if (file.type.startsWith('image/')) {
if (file.size >= props.size) {
ElMessage.error('超出文件大小限制');
res = false;
}
} else {
ElMessage.error('只允许上传图片');
res = false;
}
return res;
};
const fmtSize = computed(() => {
const units = ['byte', 'KB', 'MB', 'GB', 'TB'];
let res = props.size,
unit = 0;
while (res >= 800) {
res /= 1024;
unit++;
}
return res + units[unit];
});
</script>
<style lang="less" scoped></style>

@ -2,8 +2,8 @@
<component :is="render" />
</template>
<script setup lang="jsx">
import ElTable from './extra/ElTable.vue';
import { ElTableColumn } from 'element-plus/es/components/table/index';
import ElTable from './extra/ElTable.vue';
const props = defineProps({
/**
* 列表唯一标识
@ -241,7 +241,7 @@
watch(
() => props.total,
(value) => {
if (!value) {
if (!value && unref(search).pageIndex !== 1) {
resetPage();
}
}

@ -35,7 +35,7 @@
},
lazy: {
type: Boolean,
default: true,
default: false,
},
previewSrcList: {
type: Array,
@ -46,8 +46,10 @@
default: true,
},
});
const slots = useSlots();
const width = unref(computed(() => props.width));
const height = unref(computed(() => props.height));
const attrs = useAttrs();
const slots = useSlots();
const imageSlots = {
placeholder: () => (
<div class="image-slot">
@ -61,17 +63,51 @@
),
...slots,
};
let previewSrcList = [...props.previewSrcList];
if (previewSrcList?.length === 0) {
previewSrcList.push(props.src);
const previewSrcList = ref([]);
const count = computed(() => unref(previewSrcList).length);
watch(
() => props.previewSrcList,
(value) => {
if (value?.length === 0) {
previewSrcList.value.push(props.src);
} else {
previewSrcList.value = [...value];
}
const width = computed(() => props.width);
const height = computed(() => props.height);
const render = () => <ElImage {...{ ...props, previewSrcList }} {...attrs} v-slots={imageSlots} />;
},
{ immediate: true, deep: true }
);
const render = () =>
props.previewSrcList?.length > 0 ? (
<div class="image-container">
<ElImage {...{ ...props, previewSrcList: unref(previewSrcList) }} {...attrs} v-slots={imageSlots} />
<div class="image-count">{unref(count)}</div>
</div>
) : (
<ElImage {...{ ...props, previewSrcList: unref(previewSrcList) }} {...attrs} v-slots={imageSlots} />
);
</script>
<style lang="less" scoped>
.el-image {
.image-container {
position: relative;
margin: auto;
width: max-content;
height: max-content;
display: flex;
justify-content: center;
border-radius: @layout-space;
overflow: hidden;
:deep(.image-count) {
border-radius: @layout-space-small;
background: #ddd;
padding: 0 7px;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
bottom: 0;
right: 0;
}
:deep(.el-image) {
width: v-bind(width);
height: v-bind(height);
:deep(.image-slot) {
@ -85,4 +121,5 @@
font-size: 30px;
}
}
}
</style>

@ -1,212 +0,0 @@
<template>
<component :is="render" />
</template>
<script setup lang="jsx">
import config from '@/configs';
import { ElMessage } from '@/plugins/element-plus';
import { ElUpload } from 'element-plus/es/components/upload/index';
import 'element-plus/es/components/upload/style/css';
import { ElImage } from 'element-plus/es/components/image/index';
import 'element-plus/es/components/image/style/css';
const store = useStore();
const props = defineProps({
action: {
type: String,
default: config.baseURL + '/edu-oss/oss/fileUpload',
},
data: {
type: Object,
default() {
return { service: 'msb-edu-course' };
},
},
headers: {
type: Object,
default() {
return {};
},
},
drag: {
type: Boolean,
default: true,
},
multiple: {
type: Boolean,
defualt: false,
},
disabled: {
type: Boolean,
defualt: false,
},
limit: {
type: Number,
default: 1,
},
size: {
type: Number,
default: 1024 * 1024 * 20,
},
accept: {
type: String,
default: '*.*',
},
});
const attrs = useAttrs();
const emits = defineEmits(['update:modelValue']);
let headers = { ...props.headers };
headers['Authorization'] = 'Bearer ' + store.state.local.token;
let imgList = ref([]);
const refsUpload = ref(null);
watch(
() => imgList,
() => {
if (props.limit === 1) {
emits('update:modelValue', unref(imgList)[0]?.response.data);
} else {
emits('update:modelValue', unref(imgList));
}
},
{ deep: true }
);
const handleSuccess = (res, file, list) => {
console.info('[upload] success', list);
imgList.value = list;
};
const handleRemove = (file, list) => {
console.info('[upload] remove', list);
imgList.value = list;
};
const handleExceed = (list) => {
console.info('[upload] exceed', list);
ElMessage.error('超出最大上传数量');
};
const handleBeforeUpload = (file) => {
console.info('[upload] upload', file);
if (file.size >= props.size) {
ElMessage.error('超出文件大小限制');
return false;
}
};
const fmtSize = computed(() => {
const units = ['byte', 'KB', 'MB', 'GB', 'TB'];
let res = props.size,
unit = 0;
while (res >= 800) {
res /= 1024;
unit++;
}
return res + units[unit];
});
watch(
() => attrs.modelValue,
(value) => {
if (props.limit === 1 && value) {
imgList.value = [
{
name: value,
response: {
data: value,
},
},
];
} else {
imgList.value = value || [];
}
},
{ immediate: true, deep: true }
);
const handleDeleteImage = (index) => {
if (unref(refsUpload)) {
unref(refsUpload).handleRemove(imgList[index]);
} else {
unref(imgList).splice(index, 1);
}
};
const render = () => (
<div class="upload-box">
<div class="upload-image">
{unref(imgList).map((item, index) => (
<div class="img-li">
<ElImage src={item?.response?.data} alt={item.name} />
{!props.disabled ? (
<div class="img-li-cover" onClick={() => handleDeleteImage(index)}>
<ElIcon class="upload-del-icon" name="delete-bin-fill" size="20" />
</div>
) : (
''
)}
</div>
))}
{props.limit !== unref(imgList).length ? (
<ElUpload
ref={refsUpload}
{...{ ...props, headers }}
{...attrs}
before-upload={handleBeforeUpload}
on-exceed={handleExceed}
on-remove={handleRemove}
on-success={handleSuccess}
show-file-list={false}
>
<ElIcon class="el-icon--upload" name="add-fill" size="20" />
</ElUpload>
) : (
''
)}
</div>
<div class="el-upload__tip">支持小于 {unref(fmtSize)} 文件</div>
</div>
);
</script>
<style lang="less" scoped>
.upload-box {
:deep(.upload-image) {
display: flex;
flex-wrap: wrap;
.img-li {
display: flex;
align-items: center;
justify-content: center;
position: relative;
width: 150px;
height: 150px;
margin-right: 20px;
overflow: hidden;
.img-li-cover {
display: none;
}
&:hover {
.img-li-cover {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
cursor: pointer;
.upload-del-icon {
color: #fff;
}
}
}
}
}
}
:deep(.el-upload) {
width: 150px;
height: 150px;
.el-upload-dragger {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
.el-icon--upload {
margin: 0;
}
}
}
</style>

@ -12,7 +12,7 @@
<script setup>
import LayoutLogo from './logo.vue';
const { proxy } = getCurrentInstance();
const store = useStore();
const asideList = computed(() => store.getters['layout/asideList']);
@ -22,17 +22,21 @@
watch(
() => activeAside,
(value) => {
store.commit(
'layout/setMenuList',
unref(asideList).find((item) => item.name === unref(value))?.children || []
);
store.commit('layout/setMenuList', unref(asideList).find((item) => item.name === unref(value))?.children);
},
{ immediate: true, deep: true }
);
const handleClick = (item) => {
store.commit('layout/setAutoRouter', true);
if (unref(activeAside) === item.name) {
store.commit('layout/setActiveAside', null);
proxy.$nextTick(() => {
store.commit('layout/setActiveAside', item.name);
});
} else {
store.commit('layout/setActiveAside', item.name);
}
};
</script>

@ -10,8 +10,8 @@
</template>
<script setup>
import LayoutTitle from './title.vue';
import MenuItem from './menu-item.vue';
import LayoutTitle from './title.vue';
const router = useRouter();
const store = useStore();
const activeAside = computed(() => store.state.layout.activeAside);
@ -28,6 +28,7 @@
() => unref(menuList),
(value) => {
if (unref(autoRouter)) {
if (value) {
if (value.length === 1) {
console.info('[router] menu push 1 ' + value[0].name);
handleSelect(value[0].name);
@ -37,6 +38,7 @@
}
store.commit('layout/setAutoRouter', false);
}
}
},
{
immediate: true,

@ -1,6 +1,6 @@
<template>
<router-view v-slot="{ route, Component }">
<transition mode="out-in" name="fade-transform">
<transition mode="out-in" name="fade">
<keep-alive :exclude="exclude" :max="30">
<component :is="Component" :key="route.name" />
</keep-alive>
@ -13,4 +13,14 @@
const exclude = computed(() => store.state.layout.notKeepAliveList);
</script>
<style lang="less" scoped></style>
<style lang="less" scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.1s ease-in;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

@ -64,7 +64,31 @@ export const excel = (fileName, data) => {
navigator.msSaveBlob(blob, fileName);
}
};
/**
* 字典查询
* @param {*} dict 字典数据
* @param {*} value 字典值
* @param {*} opts 配置
* @returns 字典标签
*/
export const dict = (dict, value, opts = {}) => {
opts = { label: 'label', value: 'value', default: '未知', ...opts };
return (dict ? dict.value || dict : []).find((item) => item[opts.value] === value)?.[opts.label] || opts.default;
};
/**
* 显示姓名
* @param {*} nickname 昵称
* @param {*} realname 真实姓名
* @returns 组合显示
*/
export const name = (nickname, realname) => {
return realname ? `${nickname}${realname}` : nickname;
};
export default (app) => {
app.config.globalProperties.$copy = copy;
app.config.globalProperties.$download = download;
app.config.globalProperties.$excel = excel;
app.config.globalProperties.$dict = dict;
app.config.globalProperties.$name = name;
};

@ -81,10 +81,10 @@ router.onError((error, to) => {
router.beforeEach(async (to, from, next) => {
if (store.state.local.token) {
if (!store.state.auth.permission.length) {
await store.dispatch('auth/getUserInfo');
if (config.useLocalRouter) {
store.commit('auth/setPermission', routes);
} else {
await store.dispatch('auth/getUserInfo');
await store.dispatch('auth/getPermission');
}
next({ ...to, replace: true });

@ -15,7 +15,7 @@ export default [
component: () => import('@/views/system/user/index.vue'),
meta: {
title: '用户管理',
icon: 'user-1-fill',
icon: 'Avatar',
},
children: [
{
@ -24,7 +24,7 @@ export default [
component: () => import('@/views/system/user/form.vue'),
meta: {
title: '创建用户',
icon: 'user-1-fill',
icon: 'Avatar',
hidden: true,
},
},
@ -34,17 +34,7 @@ export default [
component: () => import('@/views/system/user/form.vue'),
meta: {
title: '编辑用户',
icon: 'user-1-fill',
hidden: true,
},
},
{
path: 'detail/:id',
name: 'UserDetail',
component: () => import('@/views/system/user/form.vue'),
meta: {
title: '用户详情',
icon: 'user-1-fill',
icon: 'Avatar',
hidden: true,
},
},

@ -16,9 +16,9 @@ const getters = {
return rootState.auth.permission.filter((item) => item.meta.global !== true);
},
menuList: (state) => {
return state.menuList.filter((item) => item.meta.hidden !== true);
return state.menuList?.filter((item) => item.meta.hidden !== true);
},
collapseMenu: (state, getters) => state.collapseMenu || getters.menuList.length < 2,
collapseMenu: (state, getters) => state.collapseMenu || getters.menuList?.length < 2,
activeAsideName: (state, getters) => getters.asideList.find((item) => item.name === state.activeAside)?.meta.title,
returnUrl: (state) => {
let res = state.returnUrl || '/';

@ -1,20 +0,0 @@
const state = () => ({
count: 0,
});
const getters = {
doubleCount: (state) => state.count * 2,
};
const mutations = {
add: (state, num = 1) => (state.count += num),
};
const actions = {
clear: ({ state, commit }) => {
commit('add', state.count * -1);
},
};
export default {
state,
getters,
mutations,
actions,
};

@ -20,7 +20,7 @@ const mutations = {
};
const actions = {
search: async ({ state, commit, rootGetters }) => {
let res = await api.findUserList({ ...rootGetters['local/page'](state.code), ...state.condition });
let res = await api.search({ ...rootGetters['local/page'](state.code), ...state.condition });
if (res) {
commit('setList', res.content);
commit('setTotal', res.totalElements);
@ -39,15 +39,18 @@ const actions = {
],
});
},
detail: async (context, id) => {
let res = await api.getUserDetail(id);
detail: async ({ dispatch }, id) => {
if (!state.list.length) {
await dispatch('search');
}
let res = state.list.find((item) => item.id === id);
if (!res) {
ElMessage.error('加载详情失败');
}
return res;
},
save: async ({ dispatch }, data) => {
let save = data.id ? api.updateUser : api.createUser;
let save = data.id ? api.update : api.create;
let res = await save(data);
if (res) {
ElMessage.success('保存成功');
@ -57,13 +60,13 @@ const actions = {
}
return res;
},
remove: async ({ dispatch }, ids) => {
if (!ids.length) {
remove: async ({ dispatch }, idList) => {
if (!idList.length) {
ElMessage.warning('请选择要删除的数据');
} else {
try {
await ElMessageBox.confirm('数据删除后无法恢复,确定要删除吗?', '危险操作');
let res = await api.removeUser(ids);
let res = await api.remove({ idList });
if (res) {
ElMessage.success('删除成功');
dispatch('search');

@ -1,34 +1,44 @@
import config from '@/configs';
import { ElMessage } from '@/plugins/element-plus';
import store from '@/store';
import qs from 'qs';
import { ElMessage } from '@/plugins/element-plus';
const handleResponse = async ({ config, headers, data, status }) => {
const handleResponse = async ({ config: requestConfig, headers, data, status }) => {
if (
['application/octet-stream', 'application/zip'].indexOf(headers['content-type']) !== -1 ||
config['down'] === 'file'
requestConfig['down'] === 'file'
) {
return data;
}
let code = data.code || status;
console.info('[api]', code, config.method, config.url, data.data);
if (code !== 200) {
ElMessage.error(data.msg || '服务器异常');
console.info('[api]', code, requestConfig.method, requestConfig.url, data.data);
if (code !== 'SUCCESS') {
ElMessage.error(data.message || '服务器异常');
switch (code) {
case 500:
case 501:
case 'SYSTEM_ERROR': //系统异常
case 'INTERFACE_SYSTEM_ERROR': // 外部接口调用异常
case 'CONNECT_TIME_OUT': // 业务连接处理超时
case 'NULL_ARGUMENT': // 参数为空
case 'ILLEGAL_ARGUMENT': // 参数不合法
case 'ILLEGAL_CONFIGURATION': // 非法配置
case 'ILLEGAL_STATE': // 非法状态
case 'ENUM_CODE_ERROR': // 错误的枚举编码
case 'LOGIC_ERROR': // 逻辑错误
case 'CONCURRENT_ERROR': // 并发异常
case 'ILLEGAL_OPERATION': // 非法操作
case 'REPETITIVE_OPERATION': // 重复操作
case 'RESOURCE_NOT_FOUND': // 资源不存在
case 'RESOURCE_ALREADY_EXIST': // 资源已存在
case 'TYPE_UN_MATCH': // 类型不匹配
case 'FILE_NOT_EXIST': // 文件不存在
case 'LIMIT_BLOCK': // 请求被限流
case 'BIZ_ERROR': // 业务处理异常
break;
case 524:
case 525:
case 50008:
case 50012:
case 50014:
case 'TOKEN_EXPIRE': // TOKEN过期
case 'TOKEN_FAIL': // TOKEN失效
case 'NO_OPERATE_PERMISSION': // 无操作权限
store.dispatch('auth/logout');
break;
case 404:
case 9999:
console.warn('接口9999', config);
break;
default:
}
}
@ -47,7 +57,7 @@ instance.interceptors.request.use(
(config) => {
const token = store.state.local.token;
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
config.headers['Authorization'] = token;
}
if (config.data && config.headers['Content-Type'] === 'application/x-www-form-urlencoded;charset=UTF-8') {
config.data = qs.stringify(config.data);

@ -21,7 +21,7 @@
</template>
</el-input>
</el-form-item>
<el-form-item prop="verifyCode">
<!-- <el-form-item prop="verifyCode">
<div class="flex">
<el-input v-model="form.verifyCode" class="ghost" placeholder="请输入验证码">
<template #prefix>
@ -32,7 +32,7 @@
{{ waitTime ? waitTime + 'S' : '发送验证码' }}
</el-button>
</div>
</el-form-item>
</el-form-item> -->
<el-button class="block" :loading="submitting" @click="handleLogin"></el-button>
</el-form>
</div>
@ -42,40 +42,41 @@
<script setup>
import config from '@/configs';
const { proxy } = getCurrentInstance();
// const { proxy } = getCurrentInstance();
const store = useStore();
const refsForm = ref(null);
const sending = ref(false);
// const sending = ref(false);
const submitting = ref(false);
const form = reactive({
phone: '',
password: '',
verifyCode: '',
});
const lastTime = computed(() => store.state.local.lastSendMessageTime);
const waitTime = ref(60);
const sendStep = ref(60);
const rules = reactive({
phone: [{ required: true, message: '请输入手机号码' }],
password: [{ required: true, message: '请输入登录密码' }],
verifyCode: [{ required: true, message: '请输入验证码' }],
// verifyCode: [{ required: true, message: '' }],
});
waitTime.value = Math.max(0, unref(sendStep) - Math.ceil((new Date().getTime() - unref(lastTime)) / 1000));
setInterval(() => {
waitTime.value = Math.max(0, unref(sendStep) - Math.ceil((new Date().getTime() - unref(lastTime)) / 1000));
}, 1000);
// const lastTime = computed(() => store.state.local.lastSendMessageTime);
// const waitTime = ref(60);
// const sendStep = ref(60);
// waitTime.value = Math.max(0, unref(sendStep) - Math.ceil((new Date().getTime() - unref(lastTime)) / 1000));
// setInterval(() => {
// waitTime.value = Math.max(0, unref(sendStep) - Math.ceil((new Date().getTime() - unref(lastTime)) / 1000));
// }, 1000);
const handleSms = async () => {
if (form.phone) {
sending.value = true;
await store.dispatch('auth/sms', { phone: form.phone, type: 1 });
sending.value = false;
} else {
proxy.$message.warning('请输入手机号码');
}
};
// const handleSms = async () => {
// if (form.phone) {
// sending.value = true;
// await store.dispatch('auth/sms', { phone: form.phone, type: 1 });
// sending.value = false;
// } else {
// proxy.$message.warning('');
// }
// };
const handleLogin = async () => {
submitting.value = true;
try {

@ -1,39 +1,7 @@
<template>
<div class="container">
<h1>{{ $route.name }}</h1>
<h1>
<el-icon name="app-store" size="30" />
<span>马士兵严选</span>
<el-icon name="msb" size="30" svg />
<el-icon color="red" name="vue" size="30" svg />
<el-icon color="red" name="Avatar" size="30" />
</h1>
<p>count:{{ count }}, double count:{{ doubleCount }}</p>
<el-button type="primary" @click="handleAdd"></el-button>
<el-button type="danger" @click="handleClear"></el-button>
<br />
<el-date-picker />
{{ form }}
<el-input v-model="form.msg" />
<el-editor v-model="form.msg" preview="pc" />
</div>
<div class="container">欢迎使用马士兵管理平台</div>
</template>
<script setup>
const store = useStore();
const count = computed(() => store.state.demo.count);
const doubleCount = computed(() => store.getters['demo/doubleCount']);
const handleAdd = () => {
store.commit('demo/add');
};
const handleClear = () => {
store.dispatch('demo/clear');
};
const form = reactive({ msg: '123' });
</script>
<script setup></script>
<style lang="less" scoped>
.container {
height: 2000px !important;
}
</style>
<style lang="less" scoped></style>

@ -4,39 +4,38 @@
ref="refsForm"
v-loading="loading"
class="form-content"
:disabled="route.name === 'UserDetail'"
label-width="100px"
:model="form"
:rules="rules"
>
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username" />
<el-form-item label="客户名称" prop="name">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="昵称" prop="nickname">
<el-input v-model="form.nickname" />
<el-form-item label="客户来源" prop="platform">
<el-select v-model="form.platform" :config="{ label: 'name', value: 'id' }" :opts="opts.platform" />
</el-form-item>
<el-form-item label="性别" prop="sex">
<el-radio-group v-model="form.sex" :opts="opts.sex" />
<el-form-item label="凭证图片" prop="voucherClueUrls">
<el-upload-image v-model="form.voucherClueUrls" :limit="9" :size="5 * 1024 * 1024" />
</el-form-item>
<el-form-item label="头像" prop="avatar">
<el-upload-image v-model="form.avatar" :disabled="route.name === 'UserDetail'" />
<el-form-item label="手机号" prop="phone">
<el-input v-model="form.phone" />
</el-form-item>
<el-form-item label="状态" prop="enabled">
<el-switch
v-model="form.enabled"
active-text="启用"
:active-value="true"
inactive-text="禁用"
:inactive-value="false"
/>
<el-form-item label="QQ号" prop="qqNo">
<el-input v-model="form.qqNo" />
</el-form-item>
<el-form-item label="微信号" prop="wechatNo">
<el-input v-model="form.wechatNo" />
</el-form-item>
<el-form-item label="加好友时间" prop="addFriendsTime">
<el-date-picker v-model="form.addFriendsTime" value-format="YYYY-MM-DD HH:mm:ss" />
</el-form-item>
<el-form-item label="备注" prop="remarks">
<el-input v-model="form.remarks" type="textarea" />
</el-form-item>
</el-form>
<div class="form-footer">
<template v-if="route.name !== 'UserDetail'">
<el-button @click="handleCancel"></el-button>
<el-button :disabled="loading" :loading="submitting" type="primary" @click="handleSave"></el-button>
</template>
<el-button v-else @click="handleClose"></el-button>
</div>
</div>
</template>
@ -52,46 +51,47 @@
const submitting = ref(false);
const refsForm = ref(null);
const form = reactive({
username: null,
nickname: null,
sex: 0,
avatar: null,
enabled: true,
id: null,
name: null,
platform: null,
voucherClueUrls: [],
phone: null,
qqNo: null,
wechatNo: null,
addFriendsTime: null,
remarks: null,
});
const rules = reactive({
username: [{ required: true, message: '用户名不能为空' }],
nickname: [{ required: true, message: '昵称不能为空' }],
sex: [{ required: true, message: '性别不能为空' }],
avatar: [{ required: true, message: '头像不能为空' }],
enabled: [{ required: true, message: '状态不能为空' }],
name: [{ required: true, message: '客户名称不能为空' }],
});
const opts = computed(() => store.state.user.opts);
const opts = computed(() => store.state.customerPool.opts);
if (!unref(opts).init) {
store.dispatch('user/load');
store.dispatch('customerPool/load');
}
/* 详情 */
watch(
() => [route.name, route.params.id],
async (value) => {
// watch
if (['UpdateUser', 'UserDetail'].includes(value[0]) && value[1]) {
loading.value = true;
let res = await store.dispatch('user/detail', +value[1]);
if (res) {
/* 数据 */
const handleLoad = async () => {
if (route.params.id) {
const id = +route.params.id;
if (form.id !== id) {
let res = await store.dispatch('customerPool/detail', id);
Object.assign(form, res);
}
loading.value = false;
}
},
{ immediate: true }
);
};
onActivated(handleLoad);
/* 交互 */
const handleSave = async () => {
submitting.value = true;
try {
await unref(refsForm).validate();
let res = await store.dispatch('user/save', unref(form));
let data = { ...unref(form) };
data.voucherClueUrls = data.voucherClueUrls || [];
data.voucherClueUrl = data.voucherClueUrls.join(',');
let res = await store.dispatch('customerPool/save', data);
if (res) {
if (!data.id) {
unref(refsForm).resetFields();
}
handleClose();
}
} catch (e) {
@ -108,7 +108,7 @@
}
};
const handleClose = () => {
router.push({ name: 'UserManagement' });
router.push({ name: 'CustomerPool' });
};
</script>

@ -4,6 +4,7 @@
:code="code"
:config="config"
:data="list"
:operation="['create', 'search']"
:reset="handleReset"
title="用户"
:total="total"
@ -13,8 +14,11 @@
>
<template #search>
<el-form inline>
<el-form-item label="用户名" prop="username">
<el-input v-model="state.condition.username" />
<el-form-item label="员工姓名" prop="employeeName">
<el-input v-model="state.condition.employeeName" />
</el-form-item>
<el-form-item label="手机号码" prop="phone">
<el-input v-model="state.condition.phone" />
</el-form-item>
</el-form>
</template>
@ -22,6 +26,7 @@
</template>
<script setup lang="jsx">
import ElButton from '@/components/extra/ElButton.vue';
const router = useRouter();
const store = useStore();
const loading = ref(false);
@ -29,9 +34,13 @@
const list = computed(() => store.state.user.list);
const total = computed(() => store.state.user.total);
const opts = computed(() => store.state.user.opts);
if (!unref(opts).init) {
store.dispatch('user/load');
}
const state = reactive({
condition: {
username: null,
employeeName: null,
phone: null,
},
});
watch(
@ -42,15 +51,10 @@
{ immediate: true, deep: true }
);
const handleReset = () => {
//
return new Promise((resolve) => {
setTimeout(() => {
state.condition = {
username: null,
employeeName: null,
phone: null,
};
resolve();
}, 1000);
});
};
const handleSearch = async () => {
loading.value = true;
@ -69,19 +73,7 @@
rows.map((item) => item.id)
);
};
const handleDetail = (row) => {
router.push({ name: 'UserDetail', params: { id: row.id } });
};
const handleEnabled = (row, enabled) => {
console.info((enabled ? 'enabled ' : 'disabled ') + row.id);
};
const config = reactive({
//
setting: false,
// ElTable
table: {},
//
page: { sizes: [20, 100] },
//
columns: [
{
@ -90,51 +82,54 @@
width: 60,
},
{
label: '用户名',
prop: 'username',
label: '员工姓名',
prop: 'employeeName',
minWidth: 160,
fixed: 'left',
},
{
label: '昵称',
prop: 'nickname',
minWidth: 160,
label: '手机',
prop: 'phone',
width: 160,
},
{
label: '性别',
slots: {
default: ({ row }) => unref(opts).sex.find((item) => item.value === row.sex)?.label,
label: '邮箱',
prop: 'email',
width: 160,
},
width: 100,
{
label: '员工类型',
prop: 'employeeType',
width: 160,
},
{
label: '头像',
slots: {
default: ({ row }) => <ElImage src={row.avatar} alt="用户头像" />,
label: '是否启用',
prop: 'isEnable',
width: 180,
},
width: 100,
{
label: '备注',
prop: 'remark',
width: 180,
},
{
label: '登录时间',
slots: {
default: ({ row }) => dayjs(row.loginTime).format('YYYY-MM-DD HH:mm:ss'),
label: '更新时间',
prop: 'updateTime',
width: 180,
},
{
label: '更新人',
prop: 'updateUser',
width: 180,
},
{
label: '状态',
slots: {
default: ({ row }) => (
<ElSwitch
modelValue={row.enabled}
active-text="启用"
inactive-text="禁用"
active-value={true}
inactive-value={false}
onInput={(e) => handleEnabled(row, e)}
/>
),
label: '创建时间',
prop: 'createTime',
width: 180,
},
{
label: '创建人',
prop: 'createUser',
width: 160,
},
{
@ -149,18 +144,10 @@
<ElButton type="text" onClick={() => handleRemove([row])}>
删除
</ElButton>
<ElDropdown
opts={[
{
label: '详情',
onClick: () => handleDetail(row),
},
]}
/>
</div>
),
},
width: 200,
width: 100,
},
],
});

@ -21,7 +21,8 @@ export default (configEnv) => {
open: false,
proxy: {
'/api': {
target: 'https://gateway-test.mashibing.cn', // 测试地址
target: 'http://192.168.10.251:4500/',
// target: 'https://gateway-test.mashibing.cn', // 测试地址
// target: 'https://gateway.mashibing.cn', // 预发地址
// target: 'https://gateway.mashibing.com', // 生产环境
changeOrigin: true,

Loading…
Cancel
Save