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 | 复制文本 | | copy | 复制文本 |
| download | 下载指定地址文件 | | download | 下载指定地址文件 |
| excel | 导出 excel 文件并下载 | | excel | 导出 excel 文件并下载 |
| dict | 字典查询 |
| name | 昵称和姓名拼接显示 |
## 全局组件 ## 全局组件
| 名称 | 功能介绍 | | 名称 | 功能介绍 |
| --------- | ------------ | | ------------- | ------------ |
| ElEditor | 富文本编辑器 | | ElEditor | 富文本编辑器 |
| ElUploadImage | 图片上传 |
| TableList | 通用列表组件 | | TableList | 通用列表组件 |
## 心得总结 ## 心得总结

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

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

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

@ -1,59 +1,29 @@
const mock = (data) => import request from '@/utils/request.js';
new Promise((resolve) => { export const search = (params) => {
setTimeout(() => { return request({
resolve(data); url: '/employee',
}, Math.random() * 1500 + 500); 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) => { export const create = (data) => {
data = { return request({
id: new Date().getTime(), url: '/employee',
...data, method: 'post',
}; 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 removeUser = (ids) => { export const update = (data) => {
list = list.filter((item) => !ids.includes(item.id)); return request({
return mock(true); url: '/employee',
method: 'put',
data,
});
}; };
export const getUserDetail = (id) => { export const remove = (params) => {
return mock(list.find((item) => item.id === id)); 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" /> <component :is="render" />
</template> </template>
<script setup lang="jsx"> <script setup lang="jsx">
import ElTable from './extra/ElTable.vue';
import { ElTableColumn } from 'element-plus/es/components/table/index'; import { ElTableColumn } from 'element-plus/es/components/table/index';
import ElTable from './extra/ElTable.vue';
const props = defineProps({ const props = defineProps({
/** /**
* 列表唯一标识 * 列表唯一标识
@ -241,7 +241,7 @@
watch( watch(
() => props.total, () => props.total,
(value) => { (value) => {
if (!value) { if (!value && unref(search).pageIndex !== 1) {
resetPage(); resetPage();
} }
} }

@ -35,7 +35,7 @@
}, },
lazy: { lazy: {
type: Boolean, type: Boolean,
default: true, default: false,
}, },
previewSrcList: { previewSrcList: {
type: Array, type: Array,
@ -46,8 +46,10 @@
default: true, default: true,
}, },
}); });
const slots = useSlots(); const width = unref(computed(() => props.width));
const height = unref(computed(() => props.height));
const attrs = useAttrs(); const attrs = useAttrs();
const slots = useSlots();
const imageSlots = { const imageSlots = {
placeholder: () => ( placeholder: () => (
<div class="image-slot"> <div class="image-slot">
@ -61,17 +63,51 @@
), ),
...slots, ...slots,
}; };
let previewSrcList = [...props.previewSrcList]; const previewSrcList = ref([]);
if (previewSrcList?.length === 0) { const count = computed(() => unref(previewSrcList).length);
previewSrcList.push(props.src); 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); { immediate: true, deep: true }
const render = () => <ElImage {...{ ...props, previewSrcList }} {...attrs} v-slots={imageSlots} />; );
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> </script>
<style lang="less" scoped> <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); width: v-bind(width);
height: v-bind(height); height: v-bind(height);
:deep(.image-slot) { :deep(.image-slot) {
@ -85,4 +121,5 @@
font-size: 30px; font-size: 30px;
} }
} }
}
</style> </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> <script setup>
import LayoutLogo from './logo.vue'; import LayoutLogo from './logo.vue';
const { proxy } = getCurrentInstance();
const store = useStore(); const store = useStore();
const asideList = computed(() => store.getters['layout/asideList']); const asideList = computed(() => store.getters['layout/asideList']);
@ -22,17 +22,21 @@
watch( watch(
() => activeAside, () => activeAside,
(value) => { (value) => {
store.commit( store.commit('layout/setMenuList', unref(asideList).find((item) => item.name === unref(value))?.children);
'layout/setMenuList',
unref(asideList).find((item) => item.name === unref(value))?.children || []
);
}, },
{ immediate: true, deep: true } { immediate: true, deep: true }
); );
const handleClick = (item) => { const handleClick = (item) => {
store.commit('layout/setAutoRouter', true); store.commit('layout/setAutoRouter', true);
if (unref(activeAside) === item.name) {
store.commit('layout/setActiveAside', null);
proxy.$nextTick(() => {
store.commit('layout/setActiveAside', item.name); store.commit('layout/setActiveAside', item.name);
});
} else {
store.commit('layout/setActiveAside', item.name);
}
}; };
</script> </script>

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

@ -1,6 +1,6 @@
<template> <template>
<router-view v-slot="{ route, Component }"> <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"> <keep-alive :exclude="exclude" :max="30">
<component :is="Component" :key="route.name" /> <component :is="Component" :key="route.name" />
</keep-alive> </keep-alive>
@ -13,4 +13,14 @@
const exclude = computed(() => store.state.layout.notKeepAliveList); const exclude = computed(() => store.state.layout.notKeepAliveList);
</script> </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); 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) => { export default (app) => {
app.config.globalProperties.$copy = copy; 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) => { router.beforeEach(async (to, from, next) => {
if (store.state.local.token) { if (store.state.local.token) {
if (!store.state.auth.permission.length) { if (!store.state.auth.permission.length) {
await store.dispatch('auth/getUserInfo');
if (config.useLocalRouter) { if (config.useLocalRouter) {
store.commit('auth/setPermission', routes); store.commit('auth/setPermission', routes);
} else { } else {
await store.dispatch('auth/getUserInfo');
await store.dispatch('auth/getPermission'); await store.dispatch('auth/getPermission');
} }
next({ ...to, replace: true }); next({ ...to, replace: true });

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

@ -16,9 +16,9 @@ const getters = {
return rootState.auth.permission.filter((item) => item.meta.global !== true); return rootState.auth.permission.filter((item) => item.meta.global !== true);
}, },
menuList: (state) => { 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, activeAsideName: (state, getters) => getters.asideList.find((item) => item.name === state.activeAside)?.meta.title,
returnUrl: (state) => { returnUrl: (state) => {
let res = state.returnUrl || '/'; 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 = { const actions = {
search: async ({ state, commit, rootGetters }) => { 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) { if (res) {
commit('setList', res.content); commit('setList', res.content);
commit('setTotal', res.totalElements); commit('setTotal', res.totalElements);
@ -39,15 +39,18 @@ const actions = {
], ],
}); });
}, },
detail: async (context, id) => { detail: async ({ dispatch }, id) => {
let res = await api.getUserDetail(id); if (!state.list.length) {
await dispatch('search');
}
let res = state.list.find((item) => item.id === id);
if (!res) { if (!res) {
ElMessage.error('加载详情失败'); ElMessage.error('加载详情失败');
} }
return res; return res;
}, },
save: async ({ dispatch }, data) => { 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); let res = await save(data);
if (res) { if (res) {
ElMessage.success('保存成功'); ElMessage.success('保存成功');
@ -57,13 +60,13 @@ const actions = {
} }
return res; return res;
}, },
remove: async ({ dispatch }, ids) => { remove: async ({ dispatch }, idList) => {
if (!ids.length) { if (!idList.length) {
ElMessage.warning('请选择要删除的数据'); ElMessage.warning('请选择要删除的数据');
} else { } else {
try { try {
await ElMessageBox.confirm('数据删除后无法恢复,确定要删除吗?', '危险操作'); await ElMessageBox.confirm('数据删除后无法恢复,确定要删除吗?', '危险操作');
let res = await api.removeUser(ids); let res = await api.remove({ idList });
if (res) { if (res) {
ElMessage.success('删除成功'); ElMessage.success('删除成功');
dispatch('search'); dispatch('search');

@ -1,34 +1,44 @@
import config from '@/configs'; import config from '@/configs';
import { ElMessage } from '@/plugins/element-plus';
import store from '@/store'; import store from '@/store';
import qs from 'qs'; 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 ( if (
['application/octet-stream', 'application/zip'].indexOf(headers['content-type']) !== -1 || ['application/octet-stream', 'application/zip'].indexOf(headers['content-type']) !== -1 ||
config['down'] === 'file' requestConfig['down'] === 'file'
) { ) {
return data; return data;
} }
let code = data.code || status; let code = data.code || status;
console.info('[api]', code, config.method, config.url, data.data); console.info('[api]', code, requestConfig.method, requestConfig.url, data.data);
if (code !== 200) { if (code !== 'SUCCESS') {
ElMessage.error(data.msg || '服务器异常'); ElMessage.error(data.message || '服务器异常');
switch (code) { switch (code) {
case 500: case 'SYSTEM_ERROR': //系统异常
case 501: 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; break;
case 524: case 'TOKEN_EXPIRE': // TOKEN过期
case 525: case 'TOKEN_FAIL': // TOKEN失效
case 50008: case 'NO_OPERATE_PERMISSION': // 无操作权限
case 50012:
case 50014:
store.dispatch('auth/logout'); store.dispatch('auth/logout');
break; break;
case 404:
case 9999:
console.warn('接口9999', config);
break;
default: default:
} }
} }
@ -47,7 +57,7 @@ instance.interceptors.request.use(
(config) => { (config) => {
const token = store.state.local.token; const token = store.state.local.token;
if (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') { if (config.data && config.headers['Content-Type'] === 'application/x-www-form-urlencoded;charset=UTF-8') {
config.data = qs.stringify(config.data); config.data = qs.stringify(config.data);

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

@ -1,39 +1,7 @@
<template> <template>
<div class="container"> <div class="container">欢迎使用马士兵管理平台</div>
<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>
</template> </template>
<script setup> <script setup></script>
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>
<style lang="less" scoped> <style lang="less" scoped></style>
.container {
height: 2000px !important;
}
</style>

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

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

@ -21,7 +21,8 @@ export default (configEnv) => {
open: false, open: false,
proxy: { proxy: {
'/api': { '/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.cn', // 预发地址
// target: 'https://gateway.mashibing.com', // 生产环境 // target: 'https://gateway.mashibing.com', // 生产环境
changeOrigin: true, changeOrigin: true,

Loading…
Cancel
Save