Initial commit

This commit is contained in:
南宁网宿科技
2023-06-03 23:57:39 +08:00
commit 13bed2bafb
1017 changed files with 175796 additions and 0 deletions
@@ -0,0 +1,300 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="740"
:visible="visible"
:confirm-loading="loading"
:title="isUpdate ? '修改分类' : '新建分类'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 6, sm: 4, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 18, sm: 20, xs: 24 } : { flex: '1' }
"
>
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
>
<a-form-item label="上级分类" name="parentId">
<a-tree-select
allow-clear
:tree-data="categoryList"
tree-default-expand-all
placeholder="请选择上级分类"
:value="form.parentId || undefined"
:dropdown-style="{ maxHeight: '360px', overflow: 'auto' }"
@update:value="(value?: number) => (form.parentId = value)"
/>
</a-form-item>
<a-form-item label="分类名称" name="title">
<a-input
allow-clear
placeholder="请输入分类名称"
v-model:value="form.title"
@pressEnter="save"
/>
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
>
<a-form-item label="排序号" name="sortNumber">
<a-input-number
:min="0"
:max="99999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</a-form-item>
<a-form-item label="是否展示">
<a-switch
checked-children="是"
un-checked-children="否"
:checked="form.status === 0"
@update:checked="updateHideValue"
/>
</a-form-item>
</a-col>
</a-row>
<div style="margin-bottom: 22px">
<a-divider />
</div>
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
>
<a-form-item label="索引图标" name="image">
<ele-image-upload
v-model:value="images"
:item-style="{ width: '45px', height: '45px' }"
:limit="1"
@upload="onUpload"
@remove="onClose"
/>
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }"
>
<a-form-item
label="备注"
name="comments"
:label-col="
styleResponsive ? { md: 3, sm: 4, xs: 24 } : { flex: '90px' }
"
:wrapper-col="
styleResponsive ? { md: 21, sm: 20, xs: 24 } : { flex: '1' }
"
>
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入备注信息"
v-model:value="form.comments"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { message } from 'ant-design-vue/es';
import type { FormInstance, Rule } from 'ant-design-vue/es/form';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import type { Category } from '@/api/goods/category/model';
import { addCategory, updateCategory } from '@/api/goods/category';
import { FILE_SERVER } from "@/config/setting";
import { ItemType } from "ele-admin-pro/es/ele-image-upload/types";
import { uploadFile } from "@/api/system/file";
import { LoginRecordParam } from "@/api/system/login-record/model";
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Category | null;
// 上级分类id
parentId?: number;
// 全部分类数据
categoryList: Category[];
}>();
// 表单数据
const { form, resetFields, assignFields } = useFormData<Category>({
title: '',
categoryId: undefined,
parentId: undefined,
image: '',
status: 0,
sortNumber: 100
});
//
const formRef = ref<FormInstance | null>(null);
// 是否是修改
const isUpdate = ref(false);
// 提交状态
const loading = ref(false);
// 已上传数据, 可赋初始值用于回显
const images = ref([]);
// 表单验证规则
const rules = reactive<Record<string, Rule[]>>({
title: [
{
required: true,
message: '请输入分类名称',
type: 'string',
trigger: 'blur'
}
],
sortNumber: [
{
required: true,
message: '请输入排序号',
type: 'number',
trigger: 'blur'
}
],
meta: [
{
type: 'string',
validator: async (_rule: Rule, value: string) => {
if (value) {
const msg = '请输入正确的JSON格式';
try {
const obj = JSON.parse(value);
if (typeof obj !== 'object' || obj === null) {
return Promise.reject(msg);
}
} catch (_e) {
return Promise.reject(msg);
}
}
return Promise.resolve();
},
trigger: 'blur'
}
]
});
// 上传文件
const onUpload = (d: ItemType) => {
uploadFile(<File>d.file)
.then((result) => {
form.image = result.path;
message.success('上传成功');
})
.catch((e) => {
message.error(e.message);
});
};
const onClose = () => {
form.image = ''
}
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const categoryForm = {
...form,
parentId: form.parentId || 0
};
const saveOrUpdate = isUpdate.value ? updateCategory : addCategory;
saveOrUpdate(categoryForm)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const updateHideValue = (value: boolean) => {
form.status = value ? 0 : 1;
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
// 头像赋值
images.value = [];
if(props.data.image){
images.value.push({ uid:1, url: props.data.image, status: '' });
}
assignFields({
...props.data,
parentId:
props.data.parentId === 0 ? undefined : props.data.parentId
});
isUpdate.value = true;
} else {
form.parentId = props.parentId;
isUpdate.value = false;
}
} else {
resetFields();
formRef.value?.clearValidate();
}
}
);
</script>
<script lang="ts">
import * as icons from '@/layout/menu-icons';
export default {
components: icons,
data() {
return {
iconData: [
{
title: '已引入的图标',
icons: Object.keys(icons)
}
]
};
}
};
</script>
+281
View File
@@ -0,0 +1,281 @@
<template>
<div class="ele-body">
<a-card :bordered="false">
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="categoryId"
:columns="columns"
:datasource="datasource"
:parse-data="parseData"
:need-page="false"
:expand-icon-column-index="1"
:expanded-row-keys="expandedRowKeys"
:scroll="{ x: 1200 }"
cache-key="proGoodsCategoryTable"
@done="onDone"
@expand="onExpand"
>
<template #toolbar>
<a-space>
<a-button type="primary" class="ele-btn-icon" @click="openEdit()">
<template #icon>
<plus-outlined />
</template>
<span>新建</span>
</a-button>
<a-button type="dashed" class="ele-btn-icon" @click="expandAll">
展开全部
</a-button>
<a-button type="dashed" class="ele-btn-icon" @click="foldAll">
折叠全部
</a-button>
<!-- 搜索表单 -->
<a-input-search
allow-clear
v-model:value="searchText"
placeholder="请输入搜索关键词"
@search="search"
@pressEnter="search"
/>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'menuType'">
<a-tag v-if="isExternalLink(record.path)" color="red">外链</a-tag>
<a-tag v-else-if="isExternalLink(record.component)" color="orange">
内链
</a-tag>
<a-tag v-else-if="isDirectory(record)" color="blue">目录</a-tag>
<a-tag v-else-if="record.menuType === 0" color="green">分类</a-tag>
<a-tag v-else-if="record.menuType === 1">按钮</a-tag>
</template>
<template v-else-if="column.key === 'title'">
<a-avatar
:size="20"
v-if="record.image"
shape="square"
:src="`${record.image}`"
/>
<a-tooltip :title="`分类ID:${record.categoryId}`">
<span>{{ record.title }}</span>
</a-tooltip>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="orange">隐藏</a-tag>
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a @click="openEdit(null, record.categoryId)">添加</a>
<a-divider type="vertical" />
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
placement="topRight"
title="确定要删除此分类吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<category-edit
v-model:visible="showEdit"
:data="current"
:parent-id="parentId"
:category-list="categoryData"
@done="reload"
/>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { message } from 'ant-design-vue/es';
import { PlusOutlined } from '@ant-design/icons-vue';
import type {
DatasourceFunction,
ColumnItem,
EleProTableDone
} from 'ele-admin-pro/es/ele-pro-table/types';
import {
messageLoading,
toDateString,
isExternalLink,
toTreeData,
eachTreeData
} from 'ele-admin-pro/es';
import type { EleProTable } from 'ele-admin-pro/es';
import CategoryEdit from './components/category-edit.vue';
import type { Category, CategoryParam } from '@/api/goods/category/model';
import { listCategory } from '@/api/goods/category';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
key: 'index',
width: 48,
align: 'center',
fixed: 'left',
hideInSetting: true,
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
},
{
title: '分类名称',
key: 'title',
showSorterTooltip: false,
ellipsis: true
},
{
title: '排序',
dataIndex: 'sortNumber',
sorter: true,
width: 180,
showSorterTooltip: false
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
sorter: true,
width: 180,
showSorterTooltip: false,
customRender: ({ text }) => ['显示', '隐藏'][text]
},
{
title: '创建时间',
dataIndex: 'createTime',
sorter: true,
showSorterTooltip: false,
ellipsis: true,
width: 180,
customRender: ({ text }) => toDateString(text)
},
{
title: '操作',
key: 'action',
width: 200,
align: 'center'
}
]);
// 当前编辑数据
const current = ref<Category | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 上级分类id
const parentId = ref<number>();
// 分类数据
const categoryData = ref<Category[]>([]);
// 表格展开的行
const expandedRowKeys = ref<number[]>([]);
// 表格数据源
const datasource: DatasourceFunction = ({ where }) => {
return listCategory({ ...where });
};
/* 数据转为树形结构 */
const parseData = (data: Category[]) => {
return toTreeData({
data: data.map((d) => {
return { ...d, key: d.categoryId, value: d.categoryId };
}),
idField: 'categoryId',
parentIdField: 'parentId'
});
};
/* 表格渲染完成回调 */
const onDone: EleProTableDone<Category> = ({ data }) => {
categoryData.value = data;
};
/* 刷新表格 */
const reload = (where?: CategoryParam) => {
tableRef?.value?.reload({ where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: Category | null, id?: number) => {
current.value = row ?? null;
parentId.value = id;
showEdit.value = true;
};
const search = (searchText) => {
reload({ title: searchText });
};
/* 删除单个 */
const remove = (row: Category) => {
if (row.children?.length) {
message.error('请先删除子节点');
return;
}
const hide = messageLoading('请求中..', 0);
removeCategory(row.categoryId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 展开全部 */
const expandAll = () => {
let keys: number[] = [];
eachTreeData(categoryData.value, (d) => {
if (d.children && d.children.length && d.categoryId) {
keys.push(d.categoryId);
}
});
expandedRowKeys.value = keys;
};
/* 折叠全部 */
const foldAll = () => {
expandedRowKeys.value = [];
};
/* 点击展开图标时触发 */
const onExpand = (expanded: boolean, record: Category) => {
if (expanded) {
expandedRowKeys.value = [
...expandedRowKeys.value,
record.categoryId as number
];
} else {
expandedRowKeys.value = expandedRowKeys.value.filter(
(d) => d !== record.categoryId
);
}
};
/* 判断是否是目录 */
const isDirectory = (d: Category) => {
return !!d.children?.length;
};
</script>
<script lang="ts">
export default {
name: 'Category'
};
</script>
@@ -0,0 +1,158 @@
<!-- 角色编辑弹窗 -->
<template>
<ele-modal
:width="460"
:visible="visible"
:confirm-loading="loading"
:title="isUpdate ? '修改角色' : '添加角色'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 5, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="角色名称" name="roleName">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入角色名称"
v-model:value="form.roleName"
/>
</a-form-item>
<a-form-item label="角色标识" name="roleCode">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入角色标识"
v-model:value="form.roleCode"
/>
</a-form-item>
<a-form-item label="备注">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入备注"
v-model:value="form.comments"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { message } from 'ant-design-vue/es';
import type { FormInstance, Rule } from 'ant-design-vue/es/form';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import { addRole, updateRole } from '@/api/system/role';
import type { Role } from '@/api/system/role/model';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Role | null;
}>();
//
const formRef = ref<FormInstance | null>(null);
// 是否是修改
const isUpdate = ref(false);
// 提交状态
const loading = ref(false);
// 表单数据
const { form, resetFields, assignFields } = useFormData<Role>({
roleId: undefined,
roleName: '',
roleCode: '',
comments: ''
});
// 表单验证规则
const rules = reactive<Record<string, Rule[]>>({
roleName: [
{
required: true,
message: '请输入角色名称',
type: 'string',
trigger: 'blur'
}
],
roleCode: [
{
required: true,
message: '请输入角色标识',
type: 'string',
trigger: 'blur'
}
]
});
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const saveOrUpdate = isUpdate.value ? updateRole : addRole;
saveOrUpdate(form)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
assignFields(props.data);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
formRef.value?.clearValidate();
}
}
);
</script>
+245
View File
@@ -0,0 +1,245 @@
<template>
<div class="ele-body">
<a-card :bordered="false">
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="commentId"
:columns="columns"
:datasource="datasource"
v-model:selection="selection"
:scroll="{ x: 800 }"
cache-key="proShopCommentTable"
>
<template #toolbar>
<a-space>
<!-- <a-button type="primary" class="ele-btn-icon" @click="openEdit()">-->
<!-- <template #icon>-->
<!-- <plus-outlined />-->
<!-- </template>-->
<!-- <span>新建</span>-->
<!-- </a-button>-->
<a-button
danger
type="primary"
class="ele-btn-icon"
@click="removeBatch"
>
<template #icon>
<delete-outlined />
</template>
<span>删除</span>
</a-button>
<a-radio-group :defaultValue="score" @change="handleTabs">
<a-radio-button :value="0">全部</a-radio-button>
<a-radio-button :value="10">好评</a-radio-button>
<a-radio-button :value="20">中评</a-radio-button>
<a-radio-button :value="30">差评</a-radio-button>
</a-radio-group>
<a-input-search
allow-clear
v-model:value="searchText"
placeholder="请输入关键词"
@search="reload"
@pressEnter="reload"
>
<template #addonBefore>
<a-select
v-model:value="type"
style="width: 100px; margin: -5px -12px"
>
<a-select-option value="goodsName">商品名称</a-select-option>
<a-select-option value="goodsCode">商品编码</a-select-option>
<a-select-option value="userId">用户ID</a-select-option>
</a-select>
</template>
</a-input-search>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
placement="topRight"
title="确定要删除此评价吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<comment-edit v-model:visible="showEdit" :data="current" @done="reload" />
</div>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue/es';
import {
PlusOutlined,
DeleteOutlined,
ExclamationCircleOutlined
} from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro/es';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import { messageLoading, toDateString } from 'ele-admin-pro/es';
import CommentEdit from './components/comment-edit.vue';
import {
pageComments,
removeComment,
removeComments
} from '@/api/goods/comment';
import type { Comment, CommentParam } from '@/api/goods/comment/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
key: 'index',
width: 48,
align: 'center',
fixed: 'left',
hideInSetting: true,
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
},
{
title: '商品信息',
dataIndex: 'goods',
showSorterTooltip: false
},
{
title: '买家',
dataIndex: 'user',
showSorterTooltip: false
},
{
title: '评分',
dataIndex: 'score',
sorter: true,
showSorterTooltip: false
},
{
title: '评价内容',
dataIndex: 'content'
},
{
title: '图片评价',
dataIndex: 'isPicture'
},
{
title: '评价时间',
dataIndex: 'createTime',
sorter: true,
showSorterTooltip: false,
ellipsis: true,
customRender: ({ text }) => toDateString(text)
},
{
title: '操作',
key: 'action',
width: 200,
align: 'center'
}
]);
// 表格选中数据
const selection = ref<Comment[]>([]);
// 当前编辑数据
const current = ref<Comment | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
const type = ref('goodsName');
const searchText = ref('');
const score = ref(0);
const handleTabs = (e) => {
score.value = e.target.value;
};
// 表格数据源
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
if (type.value == 'goodsName') {
where.goodsName = searchText.value;
}
if (type.value == 'goodsCode') {
where.goodsCode = searchText.value;
}
if (type.value == 'userId') {
where.userId = searchText.value;
}
return pageComments({ ...where, ...orders, page, limit });
};
/* 搜索 */
const reload = (where?: CommentParam) => {
selection.value = [];
tableRef?.value?.reload({ page: 1, where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: Comment) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 删除单个 */
const remove = (row: Comment) => {
const hide = messageLoading('请求中..', 0);
removeComment(row.commentId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的评价吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = messageLoading('请求中..', 0);
removeComments(selection.value.map((d) => d.commentId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
</script>
<script lang="ts">
export default {
name: 'ShopComment'
};
</script>
+433
View File
@@ -0,0 +1,433 @@
<template>
<div>
<a-form-item label="商品规格" :labelCol="labelCol" :wrapperCol="wrapperCol">
<div v-if="true" class="form-item-help" style="line-height: 36px">
<small>最多添加3个商品规格组,生成的SKU数量不能超出50个</small>
</div>
<!-- 规格组 -->
<div
class="spec-group"
>
<div class="spec-group-item clearfix">
<a-input
class="group-item-input"
:readOnly="isSpecLocked"
placeholder="请输入规格名称"
@change="onChangeSpecGroupIpt"
/>
<a
v-if="!isSpecLocked"
class="group-item-delete"
href="javascript:;"
@click="handleDeleteSpecGroup(index)"
>删除规格组</a
>
</div>
<div class="spec-value clearfix">
<div
class="spec-value-item"
>
<a-input
class="value-item-input"
:readOnly="isSpecLocked"
placeholder="请输入规格值"
@change="onChangeSpecValueIpt"
/>
<a-icon
v-if="!isSpecLocked"
class="icon-close"
theme="filled"
type="close-circle"
@click="handleDeleteSpecValue(index, idx)"
/>
</div>
<div v-if="!isSpecLocked" class="spec-value-add">
<a
class="group-item-delete"
href="javascript:;"
@click="handleAddSpecValue(index)"
>新增规格值</a
>
</div>
</div>
</div>
<!-- 添加规格按钮 -->
<a-button
v-if="!isSpecLocked && multiSpecData.specList.length < 3"
class="spec-group-add-btn"
icon="plus"
@click="handleAddSpecGroup"
>添加规格组</a-button
>
</a-form-item>
<a-form-item
v-show="multiSpecData.skuList.length"
label="SKU列表"
:labelCol="labelCol"
:wrapperCol="wrapperCol"
>
<!-- 批量设置 -->
<div v-if="multiSpecData.skuList.length > 1" class="sku-batch">
<span class="title">批量设置:</span>
<a-input-number
placeholder="商品价格"
:min="0.01"
:precision="2"
/>
<a-input-number
placeholder="成本价格"
:min="0"
:precision="2"
/>
<a-input-number
placeholder="库存数量"
:min="0"
:precision="0"
/>
<a-input-number
placeholder="商品重量"
:min="0"
:precision="2"
/>
<a-input
placeholder="sku编码"
/>
<a-button @click="handleSkuBatch">立即设置</a-button>
</div>
<!-- sku列表table -->
<a-table
class="sku-list"
:columns="multiSpecData.skuColumns"
:dataSource="multiSpecData.skuList"
:scroll="{ x: true }"
:pagination="false"
bordered
>
<!-- 预览图 -->
<template #image="text, item">
<SelectImage
v-model="item.image_id"
:defaultList="item.image_id > 0 && item.image ? [item.image] : []"
:width="50"
/>
</template>
<!-- 商品价格 -->
<template #goods_price="text, item">
<a-input-number
v-model="item.goods_price"
size="small"
:min="0.01"
:precision="2"
/>
</template>
<!-- 成本价格 -->
<template #line_price="text, item">
<a-input-number
v-model="item.line_price"
size="small"
:min="0"
:precision="2"
/>
</template>
<!-- 库存数量 -->
<template #stock_num="text, item">
<a-input-number
v-model="item.stock_num"
size="small"
:min="0"
:precision="0"
/>
</template>
<!-- 商品重量 -->
<template #goods_weight="text, item">
<a-input-number
v-model="item.goods_weight"
size="small"
:min="0"
:precision="2"
/>
</template>
<!-- sku编码 -->
<template #goods_sku_no="text, item">
<a-input v-model="item.goods_sku_no" size="small" />
</template>
</a-table>
</a-form-item>
</div>
</template>
<script>
import PropTypes from 'ant-design-vue/es/_util/vue-types';
import MultiSpecModel from '@/common/model/goods/MultiSpec';
// import { SelectImage } from '@/components';
export default {
components: {
// SelectImage
},
props: {
// 默认的规格列表
defaultSpecList: PropTypes.array.def([]),
// 默认的SKU列表
defaultSkuList: PropTypes.array.def([]),
// 商品规格是否锁定(锁定状态下不允许编辑规格)
isSpecLocked: PropTypes.bool.def(false)
},
data() {
return {
// 标签布局属性
labelCol: { span: 3 },
// 输入框布局属性
wrapperCol: { span: 21 },
// 商品多规格模型
MultiSpecModel: new MultiSpecModel(),
// MultiSpecModel: Object,
multiSpecData: {
// 规格列表
specList: [],
// SKU列表
skuList: []
}
};
},
watch: {
defaultSpecList(val) {
if (val.length && this.MultiSpecModel.isEmpty()) {
this.getData();
}
}
},
// 初始化数据
created() {
// 获取规格及SKU信息
this.getData();
},
methods: {
// 获取规格及SKU信息(展示)
getData() {
const { defaultSpecList, defaultSkuList } = this;
this.multiSpecData = this.MultiSpecModel.getData(
defaultSpecList,
defaultSkuList
);
},
// 获取规格及SKU信息(表单提交)
getFromSpecData() {
return this.MultiSpecModel.getFromSpecData();
},
// 添加规格组
handleAddSpecGroup() {
if (this.checkSkuMaxNum()) {
this.MultiSpecModel.handleAddSpecGroup();
}
},
// 删除规格组
handleDeleteSpecGroup(groupIndex) {
const app = this;
const modal = this.$confirm({
title: '您确定要删除该规格组吗?',
content: '删除后不可恢复',
onOk() {
// 删除元素
app.MultiSpecModel.handleDeleteSpecGroup(groupIndex);
// 关闭对话框
modal.destroy();
}
});
},
// 新增规格值
handleAddSpecValue(groupIndex) {
if (this.checkSkuMaxNum()) {
this.MultiSpecModel.handleAddSpecValue(groupIndex);
}
},
// 删除规格值
handleDeleteSpecValue(groupIndex, valueIndex) {
const app = this;
const modal = this.$confirm({
title: '您确定要删除该规格值吗?',
content: '删除后不可恢复',
onOk() {
// 删除元素
app.MultiSpecModel.handleDeleteSpecValue(groupIndex, valueIndex);
// 关闭对话框
modal.destroy();
}
});
},
// 规格组输入框change事件
onChangeSpecGroupIpt() {
// 更新skuList
this.MultiSpecModel.onUpdate(true);
},
// 规格值输入框change事件
onChangeSpecValueIpt(event, itm) {
// 更新skuList
this.MultiSpecModel.onUpdate(true);
},
// 验证最大sku数量
checkSkuMaxNum() {
const skuList = this.multiSpecData.skuList;
if (skuList.length >= 50) {
this.$message.error(
`生成的sku列表数量不能大于50个,当前数量:${skuList.length}个`,
2.5
);
return false;
}
return true;
},
// 批量设置sku事件
handleSkuBatch() {
this.MultiSpecModel.handleSkuBatch();
},
// 验证多规格表单
verifyForm() {
if (!this.MultiSpecModel.verifyForm()) {
this.$message.error(this.MultiSpecModel.getError(), 2);
return false;
}
return true;
}
}
};
</script>
<style lang="less" scoped>
// 商品多规格
.spec-group {
width: 895px;
margin-bottom: 15px;
line-height: normal;
.ant-input {
height: 28px;
padding: 4px 11px;
font-size: @font-size-base;
line-height: 1.5;
&::placeholder {
font-size: @font-size-base;
}
}
.form-item-help {
line-height: 36px;
}
.spec-group-item {
background: #f4f5f9;
padding: 7px 12px;
line-height: 28px;
.group-item-input {
float: left;
width: 180px;
}
.group-item-delete {
font-size: 12px;
float: right;
}
}
.spec-value {
background: #fbfbfb;
padding: 8px 35px;
.spec-value-add,
.spec-value-item {
float: left;
margin-bottom: 6px;
margin-top: 6px;
}
.spec-value-add {
height: 28px;
line-height: 28px;
a {
font-size: 12px;
}
}
.spec-value-item {
position: relative;
margin-right: 20px;
.value-item-input {
width: 186px;
}
&:hover {
.icon-close {
display: block;
}
}
.icon-close {
display: none;
position: absolute;
top: -8px;
right: -8px;
cursor: pointer;
font-size: 16px;
color: #7d7d7d;
}
}
}
}
// 添加规格按钮
.spec-group-add-btn {
font-size: @font-size-base;
}
// 批量设置sku
.sku-batch {
line-height: 40px;
margin-bottom: 12px;
.title {
line-height: 28px;
margin-right: 15px;
font-size: @font-size-base;
}
/deep/.ant-input,
/deep/.ant-input-number {
width: 120px;
margin-right: 15px;
}
/deep/.ant-input {
width: 140px;
}
}
// sku列表
.sku-list {
width: 895px;
/deep/.ant-table-thead > tr > th,
/deep/.ant-table-tbody > tr > td {
white-space: nowrap;
}
/deep/.ant-table-tbody > tr > td {
padding: 12px 18px;
}
/deep/.ant-input-sm,
/deep/.ant-input-number-sm {
height: 28px;
}
/deep/.ant-input-number-sm input {
height: 26px;
}
}
</style>
@@ -0,0 +1,39 @@
<!-- 目录选择下拉框 -->
<template>
<a-tree-select
allow-clear
tree-default-expand-all
:placeholder="placeholder"
:value="value || undefined"
:tree-data="data"
:dropdown-style="{ maxHeight: '360px', overflow: 'auto' }"
@update:value="updateValue"
/>
</template>
<script lang="ts" setup>
import type { Goods } from '@/api/goods/model';
const emit = defineEmits<{
(e: 'update:value', value?: number): void;
}>();
withDefaults(
defineProps<{
// 选中的数据(v-modal)
value?: number;
// 提示信息
placeholder?: string;
// 文章分类
data: Goods[];
}>(),
{
placeholder: '请选择上级分类'
}
);
/* 更新选中数据 */
const updateValue = (value?: number) => {
emit('update:value', value);
};
</script>
@@ -0,0 +1,45 @@
<!-- 选择下拉框 -->
<template>
<a-select
show-search
optionFilterProp="label"
:options="data"
allow-clear
:value="value"
:placeholder="placeholder"
@update:value="updateValue"
@blur="onBlur"
/>
</template>
<script lang="ts" setup>
import { getDictionaryOptions } from '@/utils/common';
const emit = defineEmits<{
(e: 'update:value', value: string): void;
(e: 'blur'): void;
}>();
withDefaults(
defineProps<{
value?: string;
placeholder?: string;
}>(),
{
placeholder: '请选择服务器配置'
}
);
// 字典数据
const data = getDictionaryOptions('serverConfiguration');
/* 更新选中数据 */
const updateValue = (value: string) => {
emit('update:value', value);
};
/* 失去焦点 */
const onBlur = () => {
emit('blur');
};
</script>
@@ -0,0 +1,51 @@
<!-- 选择下拉框 -->
<template>
<a-select
optionFilterProp="label"
:options="data"
allow-clear
:value="value"
:placeholder="placeholder"
@update:value="updateValue"
@blur="onBlur"
@change="onChange"
/>
</template>
<script lang="ts" setup>
import { getDictionaryOptions } from '@/utils/common';
const emit = defineEmits<{
(e: 'update:value', value: string): void;
(e: 'blur'): void;
(e: 'change'): void;
}>();
withDefaults(
defineProps<{
value?: string;
placeholder?: string;
}>(),
{
placeholder: '请选择客户跟进状态'
}
);
// 字典数据
const data = getDictionaryOptions('customerFollowStatus');
/* 更新选中数据 */
const updateValue = (value: string) => {
emit('update:value', value);
};
/* 失去焦点 */
const onBlur = () => {
emit('blur');
};
/* 选择事件 */
const onChange = (e) => {
emit('change', e);
};
</script>
@@ -0,0 +1,70 @@
<!-- 角色选择下拉框 -->
<template>
<a-select
optionFilterProp="label"
:options="data"
allow-clear
:value="value"
:placeholder="placeholder"
@update:value="updateValue"
@blur="onBlur"
@change="onChange"
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { message } from 'ant-design-vue';
import { listDictionaryData } from '@/api/system/dictionary-data';
import type { SelectProps } from 'ant-design-vue';
const emit = defineEmits<{
(e: 'update:value', value: string): void;
(e: 'blur'): void;
(e: 'change'): void;
}>();
withDefaults(
defineProps<{
value?: string;
placeholder?: string;
}>(),
{
placeholder: '请选择客户来源'
}
);
// 字典数据
const data = ref<SelectProps['options']>([]);
/* 更新选中数据 */
const updateValue = (value: string) => {
emit('update:value', value);
};
/* 获取字典数据 */
listDictionaryData({
dictCode: 'customerSource'
})
.then((list) => {
data.value = list.map((d) => {
return {
value: d.dictDataCode,
label: d.dictDataName
};
});
})
.catch((e) => {
message.error(e.message);
});
/* 失去焦点 */
const onBlur = () => {
emit('blur');
};
/* 选择事件 */
const onChange = (e) => {
emit('change', e);
};
</script>
@@ -0,0 +1,45 @@
<!-- 角色选择下拉框 -->
<template>
<a-select
show-search
optionFilterProp="label"
:options="data"
allow-clear
:value="value"
:placeholder="placeholder"
@update:value="updateValue"
@blur="onBlur"
/>
</template>
<script lang="ts" setup>
import { getDictionaryOptions } from '@/utils/common';
const emit = defineEmits<{
(e: 'update:value', value: string): void;
(e: 'blur'): void;
}>();
withDefaults(
defineProps<{
value?: string;
placeholder?: string;
}>(),
{
placeholder: '请选择状态'
}
);
// 字典数据
const data = getDictionaryOptions('status');
/* 更新选中数据 */
const updateValue = (value: string) => {
emit('update:value', value);
};
/* 失去焦点 */
const onBlur = () => {
emit('blur');
};
</script>
@@ -0,0 +1,63 @@
<!-- 角色选择下拉框 -->
<template>
<a-select
optionFilterProp="label"
:options="data"
allow-clear
:value="value"
:placeholder="placeholder"
@update:value="updateValue"
@blur="onBlur"
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { message } from 'ant-design-vue';
import { listDictionaryData } from '@/api/system/dictionary-data';
import type { SelectProps } from 'ant-design-vue';
const emit = defineEmits<{
(e: 'update:value', value: string): void;
(e: 'blur'): void;
}>();
withDefaults(
defineProps<{
value?: string;
placeholder?: string;
}>(),
{
placeholder: '请选择客户类型'
}
);
// 字典数据
const data = ref<SelectProps['options']>([]);
/* 更新选中数据 */
const updateValue = (value: string) => {
emit('update:value', value);
};
/* 获取字典数据 */
listDictionaryData({
dictCode: 'customerType'
})
.then((list) => {
data.value = list.map((d) => {
return {
value: d.dictDataCode,
label: d.dictDataName
};
});
})
.catch((e) => {
message.error(e.message);
});
/* 失去焦点 */
const onBlur = () => {
emit('blur');
};
</script>
@@ -0,0 +1,76 @@
<!-- 角色选择下拉框 -->
<template>
<a-select
show-search
optionFilterProp="label"
:options="data"
allow-clear
:value="value"
:placeholder="placeholder"
@update:value="updateValue"
@search="onSearch"
@blur="onBlur"
/>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { message } from 'ant-design-vue';
import { listUsers } from '@/api/system/user';
import type { SelectProps } from 'ant-design-vue';
import { UserParam } from '@/api/system/user/model';
const emit = defineEmits<{
(e: 'update:value', value: string): void;
(e: 'blur'): void;
}>();
withDefaults(
defineProps<{
value?: string;
placeholder?: string;
}>(),
{
placeholder: '请选择订单类型'
}
);
// 字典数据
const data = ref<SelectProps['options']>([]);
/* 更新选中数据 */
const updateValue = (value: string) => {
emit('update:value', value);
};
// 默认搜索条件
const where = ref<UserParam>({});
const search = () => {
/* 获取用户列 */
listUsers({ ...where?.value })
.then((result) => {
data.value = result?.map((d) => {
return {
value: d.userId,
label: d.nickname
};
});
})
.catch((e) => {
message.error(e.message);
});
};
const onSearch = (e) => {
where.value.nickname = e;
search();
};
search();
/* 失去焦点 */
const onBlur = () => {
emit('blur');
};
</script>
@@ -0,0 +1,70 @@
<!-- 选择下拉框 -->
<template>
<a-select
show-search
optionFilterProp="label"
:options="data"
allow-clear
:value="value"
:placeholder="placeholder"
@update:value="updateValue"
@blur="onBlur"
@search="onSearch"
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { message } from 'ant-design-vue';
import { pageCustomer } from '@/api/oa/customer';
import type { SelectProps } from 'ant-design-vue';
const emit = defineEmits<{
(e: 'update:value', value: string): void;
(e: 'blur'): void;
}>();
withDefaults(
defineProps<{
value?: number;
placeholder?: string;
}>(),
{
placeholder: '请选择所属客户'
}
);
// 字典数据
const data = ref<SelectProps['options']>([]);
/* 更新选中数据 */
const updateValue = (value: string) => {
emit('update:value', value);
};
/* 获取客户列表数据 */
pageCustomer({ progress: '4' })
.then((result) => {
data.value = result?.list.map((d) => {
return {
value: d.customerId,
label: d.customerName
};
});
})
.catch((e) => {
message.error(e.message);
});
/* 失去焦点 */
const onBlur = () => {
emit('blur');
};
// 联动搜索
const onSearch = (customerName) => {
pageCustomer({ customerName }).then((res) => {
console.log(res);
});
};
</script>
@@ -0,0 +1,53 @@
<!-- 选择下拉框 -->
<template>
<a-select
show-search
optionFilterProp="label"
:options="data"
allow-clear
:value="value"
:placeholder="placeholder"
@update:value="updateValue"
@blur="onBlur"
/>
</template>
<script lang="ts" setup>
import { getDictionaryOptions } from '@/utils/common';
import { ref } from "vue";
import { SelectProps } from "ant-design-vue";
const emit = defineEmits<{
(e: 'update:value', value: string): void;
(e: 'blur'): void;
}>();
withDefaults(
defineProps<{
value?: string;
placeholder?: string;
}>(),
{
placeholder: '请选择运费模板'
}
);
// 字典数据
const data = ref<SelectProps['options']>([
{
label: '全国包邮(除偏远地区)',
text: '全国包邮(除偏远地区)',
value: 0
}
]);
/* 更新选中数据 */
const updateValue = (value: string) => {
emit('update:value', value);
};
/* 失去焦点 */
const onBlur = () => {
emit('blur');
};
</script>
@@ -0,0 +1,648 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
:width="880"
:visible="visible"
:confirm-loading="loading"
:maxable="maxAble"
:title="isUpdate ? '编辑商品' : '添加商品'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
:maskClosable="false"
@ok="save"
>
<a-tabs v-model:activeKey="activeKey" type="card">
<a-tab-pane key="1" tab="基本信息">
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="{ md: { span: 8 }, sm: { span: 24 } }"
:wrapper-col="{ md: { span: 24 }, sm: { span: 24 } }"
layout="vertical"
>
<a-row :gutter="16">
<a-col
v-bind="
styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }
"
>
<a-form-item label="商品名称" name="goodsName">
<a-input
allow-clear
placeholder="请输入商品名称"
v-model:value="form.goodsName"
/>
</a-form-item>
<a-form-item label="商品分类" name="categoryId">
<category-select
:data="categoryList"
placeholder="请选择商品分类"
v-model:value="form.categoryId"
/>
</a-form-item>
<a-form-item label="商品类型" name="goodsType">
<a-radio-group v-model:value="form.goodsType">
<a-radio :value="1">实物商品</a-radio>
<a-radio :value="0">虚拟商品</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item
label="配送方式"
name="deliveryType"
v-if="form.goodsType === 1"
>
<a-radio-group v-model:value="form.deliveryType">
<a-radio :value="0">系统默认</a-radio>
<a-radio :value="1">单独配送</a-radio>
</a-radio-group>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }
"
>
<a-form-item label="商品编码" name="goodsCode">
<a-input
allow-clear
placeholder="请输入商品编码"
:disabled="isUpdate"
v-model:value="form.goodsCode"
/>
</a-form-item>
<a-form-item label="商品状态" name="status">
<a-radio-group v-model:value="form.status">
<a-radio :value="0">上架</a-radio>
<a-radio :value="1">下架</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item
label="运费模板"
name="deliveryId"
v-if="form.goodsType === 1"
>
<deliver-select v-model:value="form.deliveryId" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="商品图片" name="images">
<ele-image-upload
v-model:value="images"
:limit="14"
:drag="true"
:multiple="true"
:upload-handler="uploadHandler"
@upload="onUpload"
/>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="2" tab="规格/库存">
<a-form
ref="formRef"
:label-col="{ md: { span: 8 }, sm: { span: 24 } }"
:wrapper-col="{ md: { span: 24 }, sm: { span: 24 } }"
layout="vertical"
:model="form"
:rules="rules"
>
<a-form-item label="规格类型" name="specType">
<a-radio-group v-model:value="form.specType">
<a-radio :value="0">单规格</a-radio>
<!-- <a-radio :value="1">多规格</a-radio>-->
</a-radio-group>
</a-form-item>
<div style="margin-bottom: 22px">
<a-divider />
</div>
<a-row :gutter="16">
<a-col
v-bind="
styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }
"
>
<a-form-item
label="商品价格"
name="goodsPriceMin"
v-if="form.specType === 0"
>
<a-input-number
:min="0.01"
:precision="2"
style="width: 200px"
v-model:value="form.goodsPriceMin"
/>
<span class="ml-10">元</span>
</a-form-item>
<a-form-item
label="划线价"
name="linePrice"
v-if="form.specType === 0"
>
<a-input-number
:min="0.01"
:precision="2"
style="width: 200px"
v-model:value="form.linePriceMin"
/>
<span class="ml-10">元</span>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 12 }
"
>
<a-form-item
label="当前库存数量"
name="stockTotal"
v-if="form.specType === 0"
>
<a-input-number
:min="0"
:precision="0"
style="width: 200px"
v-model:value="form.stockTotal"
/>
<span class="ml-10">件</span>
</a-form-item>
<a-form-item
label="商品重量"
name="goodsWeight"
v-if="form.specType === 0"
>
<a-input-number
:min="0"
:precision="0"
style="width: 200px"
v-model:value="form.goodsWeight"
/>
<span class="ml-10">千克 (Kg)</span>
</a-form-item>
</a-col>
</a-row>
<a-form-item
label="商品规格"
name="deductStockType"
v-if="form.specType === 1"
>
<!-- <MultiSpec ref="MultiSpec" />-->
</a-form-item>
<div style="margin-bottom: 22px">
<a-divider />
</div>
<a-form-item label="库存计算方式" name="deductStockType">
<a-radio-group v-model:value="form.deductStockType">
<a-radio :value="10">下单减库存</a-radio>
<a-radio :value="20">付款减库存</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="商品限购" name="purchaseLimit">
<a-radio-group v-model:value="form.purchaseLimit">
<a-radio :value="0">关闭</a-radio>
<a-radio :value="1">开启</a-radio>
</a-radio-group>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="3" tab="商品详情">
<a-form
ref="formRef"
:label-col="{ md: { span: 8 }, sm: { span: 24 } }"
:wrapper-col="{ md: { span: 24 }, sm: { span: 24 } }"
layout="vertical"
:model="form"
:rules="rules"
>
<a-form-item label="" name="content">
<!-- 编辑器 -->
<tinymce-editor
v-model:value="content"
:disabled="disabled"
:init="config"
/>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="4" tab="更多设置">
<a-form
ref="formRef"
:label-col="{ md: { span: 8 }, sm: { span: 24 } }"
:wrapper-col="{ md: { span: 24 }, sm: { span: 24 } }"
layout="vertical"
:model="form"
:rules="rules"
>
<a-form-item label="商品卖点" name="sellingPoint">
<a-input
allow-clear
placeholder="【入手皆因热爱】限时限量领券至高立减400元,限时优惠,限量抢购!!赶快下单吧!"
v-model:value="form.sellingPoint"
/>
</a-form-item>
<a-form-item label="排序号" name="sortNumber">
<a-input-number
:min="0"
:max="99999"
class="ele-fluid"
placeholder="请输入排序号(数字越小越靠前)"
v-model:value="form.sortNumber"
/>
</a-form-item>
</a-form>
</a-tab-pane>
</a-tabs>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { message } from 'ant-design-vue';
import type { Goods } from '@/api/goods/model';
import { addGoods, updateGoods } from '@/api/goods';
import { FormInstance, Rule, RuleObject } from 'ant-design-vue/es/form';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { uploadFile } from '@/api/system/file';
import { FILE_THUMBNAIL } from '@/config/setting';
import useFormData from '@/utils/use-form-data';
import CategorySelect from './category-select.vue';
import TinymceEditor from '@/components/TinymceEditor/index.vue';
import { Category } from '@/api/goods/category/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import DeliverSelect from './deliver-select.vue';
import { createCode } from '@/utils/common';
// import MultiSpec from './MultiSpec.vue';
// 是否是修改
const isUpdate = ref(false);
// 是否显示最大化切换按钮
const maxAble = ref(true);
const disabled = ref(false);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Goods | null;
categoryList?: Category[];
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
// 提交状态
const loading = ref(false);
// 已上传数据
const images = ref<ItemType[]>([]);
const formRef = ref<FormInstance | null>(null);
// 选项卡位置
const activeKey = ref('1');
// 编辑器内容,双向绑定
const content = ref<any>('');
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单数据
const { form, resetFields, assignFields } = useFormData<Goods>({
goodsType: 1,
goodsName: '',
goodsCode: `G${createCode()}`,
categoryId: 0,
image: '',
files: '',
goodsPriceMin: 0.01,
linePriceMin: 0.01,
createTime: '',
comments: '',
sortNumber: 100,
status: 0,
goodsId: 0,
specType: 0,
deliveryType: 0,
content: '',
deliveryId: 0,
sellingPoint: '',
purchaseLimit: 0,
deductStockType: 10,
salesActual: 180,
goodsWeight: 0,
stockTotal: 2000
});
/* 上传事件 */
const uploadHandler = (file: File) => {
const item: ItemType = {
file,
uid: (file as any).uid,
name: file.name
};
if (!file.type.startsWith('image')) {
message.error('只能选择图片');
return;
}
if (file.size / 1024 / 1024 > 2) {
message.error('大小不能超过 2MB');
return;
}
onUpload(item);
};
// 上传文件
const onUpload = (item) => {
const { file } = item;
uploadFile(file)
.then((data) => {
images.value.push({
uid: data.id,
url: FILE_THUMBNAIL + data.path,
status: 'done'
});
message.success('上传成功');
})
.catch((e) => {
message.error(e.message);
});
};
// 表单验证规则
const rules = reactive<Record<string, Rule[]>>({
goodsName: [
{
required: true,
message: '请输入商品名称',
type: 'string',
trigger: 'blur'
}
],
categoryId: [
{
required: true,
type: 'number',
message: '请选择商品分类',
trigger: 'blur'
}
],
goodsCode: [
{
required: true,
type: 'string',
message: '请填写商品编号',
trigger: 'blur'
}
],
goodsType: [
{
required: true,
type: 'number',
message: '请选择商品类型',
trigger: 'blur'
}
],
deliveryType: [
{
required: true,
type: 'number',
message: '请选择配送方式',
trigger: 'blur'
}
],
goodsPriceMin: [
{
required: true,
type: 'number',
message: '请输入商品价格',
trigger: 'blur'
}
],
stockTotal: [
{
required: true,
type: 'number',
message: '请输入商品库存',
trigger: 'blur'
}
],
specType: [
{
required: true,
type: 'number',
message: '请选择规格类型',
trigger: 'blur'
}
],
deliveryId: [
{
required: true,
type: 'number',
message: '请选择运费模板',
trigger: 'blur'
}
],
status: [
{
required: true,
type: 'number',
message: '请选择商品状态',
trigger: 'blur'
}
],
sortNumber: [
{
required: true,
type: 'number',
message: '请输入排序号',
trigger: 'blur'
}
],
deductStockType: [
{
required: true,
type: 'number',
message: '请设置库存计算方式',
trigger: 'blur'
}
],
purchaseLimit: [
{
required: true,
type: 'number',
message: '请设置是否限购',
trigger: 'blur'
}
],
images: [
{
required: true,
type: 'string',
message: '请选择商品图片',
trigger: 'blur',
validator: async (_rule: RuleObject, value: string) => {
if (images.value.length == 0) {
return Promise.reject('请上传商品图片');
}
return Promise.resolve();
}
}
],
customerId: [
{
required: true,
type: 'number',
message: '请选择客户'
}
],
linePriceMin: [
{
required: true,
type: 'number',
message: '请输入商品划线价',
trigger: 'blur'
}
],
content: [
{
required: true,
type: 'string',
message: '请输入文章内容',
trigger: 'blur',
validator: async (_rule: RuleObject, value: string) => {
if (content.value == '') {
return Promise.reject('请输入文字内容');
}
return Promise.resolve();
}
}
],
brand: [
{
required: true,
type: 'string',
message: '请选项商品厂商',
trigger: 'blur'
}
]
});
const editorRef = ref<InstanceType<typeof TinymceEditor> | null>(null);
const config = ref({
height: 500,
// 自定义文件上传(这里使用把选择的文件转成 blob 演示)
file_picker_callback: (callback: any, _value: any, meta: any) => {
const input = document.createElement('input');
input.setAttribute('type', 'file');
// 设定文件可选类型
if (meta.filetype === 'image') {
input.setAttribute('accept', 'image/*');
} else if (meta.filetype === 'media') {
input.setAttribute('accept', 'video/*');
}
input.onchange = () => {
const file = input.files?.[0];
if (!file) {
return;
}
if (meta.filetype === 'media') {
if (!file.type.startsWith('video/')) {
editorRef.value?.alert({ content: '只能选择视频文件' });
return;
}
}
if (file.size / 1024 / 1024 > 20) {
editorRef.value?.alert({ content: '大小不能超过 20MB' });
return;
}
const reader = new FileReader();
reader.onload = (e) => {
if (e.target?.result != null) {
const blob = new Blob([e.target.result], { type: file.type });
callback(URL.createObjectURL(blob));
}
};
reader.readAsArrayBuffer(file);
};
input.click();
}
});
// const onSelect = (item) => {
// form.goodsId = item.goodsId;
// };
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
// 获取第一张图片作为封面图
images.value.map((d, i) => {
if (i === 0) {
form.image = d.url;
}
});
const goodsForm = {
...form,
files: JSON.stringify(images.value),
content: content.value
};
const saveOrUpdate = isUpdate.value ? updateGoods : addGoods;
saveOrUpdate(goodsForm)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
loading.value = false;
assignFields({
...props.data,
files: JSON.parse(String(props.data.files))
});
images.value = JSON.parse(String(props.data.files));
content.value = props.data.content;
isUpdate.value = true;
} else {
isUpdate.value = false;
content.value = '';
}
} else {
resetFields();
formRef.value?.clearValidate();
}
}
);
</script>
<style lang="less">
.tab-pane {
min-height: 300px;
}
.ml-10 {
margin-left: 5px;
}
</style>
@@ -0,0 +1,243 @@
<!-- 用户编辑弹窗 -->
<template>
<a-drawer
width="75%"
:visible="visible"
:confirm-loading="loading"
:maxable="maxAble"
:title="'商品详情'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
:footer="null"
>
<a-form
:label-col="{ md: { span: 4 }, sm: { span: 24 } }"
:wrapper-col="{ md: { span: 19 }, sm: { span: 24 } }"
>
<div class="base-form" style="margin-bottom: 20px">
<a-descriptions :title="assets.name" bordered>
<a-descriptions-item label="商品名称">
{{ assets.name }}
</a-descriptions-item>
<a-descriptions-item label="商品IP">
{{ assets.code }}
</a-descriptions-item>
<a-descriptions-item label="状态">
<a-tag v-if="assets.status === '0'" color="green">正常</a-tag>
<a-tag v-if="assets.status === '1'" color="red">过期</a-tag>
<a-tag v-if="assets.status === '2'" color="purple">不再续</a-tag>
</a-descriptions-item>
<a-descriptions-item label="宝塔面板">
<a href="#" @click="openUrl(assets)">{{ assets.panel }}</a>
</a-descriptions-item>
<a-descriptions-item label="宝塔账号">
{{ assets.panelAccount }}
</a-descriptions-item>
<a-descriptions-item label="宝塔密码">
<span v-role="['admin']">{{ assets.panelPassword }}</span>
<span v-any-role="['user', 'guest', 'superAdmin', 'developer']"
>****</span
>
</a-descriptions-item>
<a-descriptions-item label="品牌厂商">
{{ assets.brand }}
</a-descriptions-item>
<a-descriptions-item label="商品配置">
{{ assets.configuration }}
</a-descriptions-item>
<a-descriptions-item label="开发端口">
{{ assets.openPort }}
</a-descriptions-item>
<a-descriptions-item label="有效期">
<div v-if="assets.startTime && assets.endTime">
<div class="ele-text-info">{{ assets.startTime }}</div>
<div v-html="getEndTime(assets.endTime)"></div>
</div>
</a-descriptions-item>
<a-descriptions-item label="商品账号">
<span class="ele-text-danger">root</span>
</a-descriptions-item>
<a-descriptions-item label="商品密码">
<span class="ele-text-danger" v-role="['admin']">{{
assets.password
}}</span>
<span v-any-role="['user', 'guest', 'superAdmin', 'developer']"
>****</span
>
</a-descriptions-item>
<a-descriptions-item label="创建时间">
{{ timeAgo(assets.createTime) }}
</a-descriptions-item>
<a-descriptions-item label="排序">
{{ assets.sortNumber }}
</a-descriptions-item>
<a-descriptions-item label="发布人">
{{ assets.userId }}
</a-descriptions-item>
<a-descriptions-item span="1" label="备注">
{{ assets.comments }}
</a-descriptions-item>
</a-descriptions>
</div>
<a-space style="display: none">
<a-tabs v-model:activeKey="activeKey" v-role="'admin'">
<a-tab-pane key="1" tab="宝塔面板" force-render>
<a-descriptions title="宝塔面板" class="tab-pane">
<a-descriptions-item label="宝塔面板">
<a href="#" @click="openUrl(assets)">{{ assets.panel }}</a>
</a-descriptions-item>
<a-descriptions-item label="宝塔面板账号">
{{ assets.panelAccount }}
</a-descriptions-item>
<a-descriptions-item label="宝塔面板密码">
{{ assets.panelPassword }}
</a-descriptions-item>
<a-descriptions-item label="开发端口">
{{ assets.openPort }}
</a-descriptions-item>
</a-descriptions>
</a-tab-pane>
<a-tab-pane key="2" tab="账号密码" force-render>
<a-descriptions title="商品账号密码" class="tab-pane">
<a-descriptions-item label="商品账号">
root
</a-descriptions-item>
<a-descriptions-item label="商品密码">
{{ assets.password }}
</a-descriptions-item>
</a-descriptions>
<a-descriptions title="账号密码" class="tab-pane">
<a-descriptions-item label="登录账号">
{{ assets.brandAccount }}
</a-descriptions-item>
<a-descriptions-item label="登录密码">
{{ assets.brandPassword }}
</a-descriptions-item>
</a-descriptions>
</a-tab-pane>
<a-tab-pane key="3" tab="财务信息">
<a-descriptions title="财务信息" class="tab-pane">
<a-descriptions-item label="合同金额">
<span class="ele-text-warning">
¥{{ assets.financeAmount }}
</span>
</a-descriptions-item>
<a-descriptions-item label="购买年限">
{{ assets.financeYears }}年
</a-descriptions-item>
<a-descriptions-item label="每年续费金额">
<span class="ele-text-warning">
¥{{ assets.financeRenew }}
</span>
</a-descriptions-item>
<a-descriptions-item label="客户名称">
{{ assets.financeCustomerName }}
</a-descriptions-item>
<a-descriptions-item label="客户联系人">
{{ assets.financeCustomerContact }}
</a-descriptions-item>
<a-descriptions-item label="联系方式">
{{ assets.financeCustomerPhone }}
</a-descriptions-item>
</a-descriptions>
</a-tab-pane>
</a-tabs>
</a-space>
</a-form>
</a-drawer>
</template>
<script lang="ts" setup>
import { getEndTime } from '@/utils/common';
import { ref, reactive, watch } from 'vue';
import { Form } from 'ant-design-vue';
import { assignObject, timeAgo } from 'ele-admin-pro';
import type { Assets } from '@/api/oa/assets/model';
const useForm = Form.useForm;
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Assets | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 用户信息
const assets = reactive<Assets>({
code: '',
createTime: '',
startTime: '',
endTime: '',
name: '',
brand: undefined,
account: '',
password: '',
configuration: '',
panelAccount: '',
panelPassword: '',
panel: '',
financeAmount: 0,
financeYears: 0,
financeRenew: 0,
financeCustomerName: '',
financeCustomerContact: '',
financeCustomerPhone: '',
brandAccount: '',
brandPassword: '',
openPort: '',
comments: '',
root: 'root',
userId: 0,
sortNumber: undefined,
status: undefined,
type: '',
assetsId: 0
});
// 请求状态
const loading = ref(true);
// 是否显示最大化切换按钮
const maxAble = ref(true);
// 选项卡位置
const activeKey = ref('1');
const { resetFields } = useForm(assets);
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
/* 打开外部链接 */
const openUrl = (record) => {
window.open(record.panel);
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
loading.value = false;
assignObject(assets, props.data);
}
} else {
resetFields();
}
}
);
</script>
<style lang="less">
.tab-pane {
min-height: 100px;
}
.important {
font-weight: bold;
}
</style>
@@ -0,0 +1,65 @@
<!-- 搜索表单 -->
<template>
<a-form
:label-col="{ md: { span: 6 }, sm: { span: 24 } }"
:wrapper-col="{ md: { span: 18 }, sm: { span: 24 } }"
>
<a-row>
<a-col :lg="6" :md="12" :sm="24" :xs="24">
<a-form-item label="服务器名称">
<a-input
v-model:value.trim="where.name"
placeholder="请输入服务器名称"
allow-clear
@pressEnter="search"
/>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :sm="24" :xs="24">
<a-form-item label="IP地址">
<a-input
v-model:value.trim="where.code"
placeholder="请输入服务器IP"
allow-clear
@pressEnter="search"
/>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :sm="24" :xs="24">
<a-form-item class="ele-text-right" :wrapper-col="{ span: 24 }">
<a-space>
<a-button type="primary" @click="search">查询</a-button>
<a-button @click="reset">重置</a-button>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
</template>
<script lang="ts" setup>
import useSearch from '@/utils/use-search';
import type { AssetsParam } from '@/api/oa/assets/model';
const emit = defineEmits<{
(e: 'search', where?: AssetsParam): void;
}>();
// 表单数据
const { where, resetFields } = useSearch<AssetsParam>({
name: '',
code: '',
isExpire: ''
});
/* 搜索 */
const search = () => {
emit('search', where);
};
/* 重置 */
const reset = () => {
resetFields();
search();
};
</script>
@@ -0,0 +1,289 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
:width="680"
:visible="visible"
:confirm-loading="loading"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑订单' : '添加订单'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-space>
<a-form
:label-col="{ md: { span: 7 }, sm: { span: 4 }, xs: { span: 24 } }"
:wrapper-col="{ md: { span: 17 }, sm: { span: 20 }, xs: { span: 24 } }"
>
<a-row :gutter="16">
<a-col :md="12" :sm="24" :xs="24">
<a-form-item label="订单名称" v-bind="validateInfos.customerName">
<a-input
allow-clear
:maxlength="30"
placeholder="请输入订单名称"
v-model:value="form.customerName"
@blur="
validate('customerName', { trigger: 'blur' }).catch(() => {})
"
/>
</a-form-item>
<a-form-item label="订单标识" v-bind="validateInfos.customerCode">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入用户账号"
v-model:value="form.customerCode"
/>
</a-form-item>
<a-form-item label="跟进状态" v-bind="validateInfos.progress">
<progress-select
v-model:value="form.progress"
@blur="
validate('progress', { trigger: 'blur' }).catch(() => {})
"
/>
</a-form-item>
<a-form-item label="订单类型" v-bind="validateInfos.customerType">
<type-select
v-model:value="form.customerType"
@blur="
validate('customerType', { trigger: 'blur' }).catch(() => {})
"
/>
</a-form-item>
<a-form-item label="订单来源" v-bind="validateInfos.customerSource">
<source-select
v-model:value="form.customerSource"
@blur="
validate('customerSource', { trigger: 'blur' }).catch(() => {})
"
/>
</a-form-item>
<a-form-item label="头像" v-bind="validateInfos.customerAvatar">
<ele-image-upload
v-model:value="images"
:item-style="{ width: '90px', height: '90px' }"
:limit="1"
@upload="onUpload"
/>
</a-form-item>
</a-col>
<a-col :md="12" :sm="24" :xs="24">
<a-form-item label="联系人" v-bind="validateInfos.customerContacts">
<a-input
allow-clear
:maxlength="20"
placeholder="请填写联系人"
v-model:value="form.customerContacts"
/>
</a-form-item>
<a-form-item label="手机号码" v-bind="validateInfos.customerMobile">
<a-input
allow-clear
:maxlength="20"
placeholder="请填写联系人手机号码"
v-model:value="form.customerMobile"
/>
</a-form-item>
<a-form-item
label="联系地址"
v-bind="validateInfos.customerAddress"
>
<a-input
allow-clear
placeholder="请填写联系地址"
v-model:value="form.customerAddress"
/>
</a-form-item>
<a-form-item label="公司座机" v-bind="validateInfos.customerPhone">
<a-input
allow-clear
:maxlength="20"
placeholder="请填写公司座机电话"
v-model:value="form.customerPhone"
/>
</a-form-item>
<a-form-item label="排序" v-bind="validateInfos.sortNumber">
<a-input
allow-clear
:maxlength="20"
placeholder="排序"
v-model:value="form.sortNumber"
/>
</a-form-item>
<a-form-item label="备注" v-bind="validateInfos.comments">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入备注"
v-model:value="form.comments"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-space>
</ele-modal>
</template>
<script lang="ts" setup>
import {ref, reactive, watch, computed} from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject } from 'ele-admin-pro';
import TypeSelect from './customer-edit/type-select.vue';
import ProgressSelect from './customer-edit/progress-select.vue';
import SourceSelect from './customer-edit/source-select.vue';
import { addCustomer, updateCustomer } from '@/api/oa/customer';
import type { Customer } from '@/api/oa/customer/model';
import { createCode } from '@/utils/common';
import { uploadFile } from '@/api/system/file';
import type { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FILE_SERVER } from '@/config/setting';
import { useUserStore } from '@/store/modules/user';
const userStore = useUserStore();
// 当前用户信息
const loginUser = computed(() => userStore.info ?? {});
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Customer | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 用户信息
const form = reactive<Customer>({
customerCode: '',
customerName: '',
customerType: undefined,
progress: undefined,
customerMobile: '',
customerAvatar: '',
customerPhone: '',
customerContacts: '',
customerAddress: '',
comments: '',
status: '0',
sortNumber: 100,
customerId: 0,
userId: '',
});
// 已上传数据, 可赋初始值用于回显
const images = ref(<any>[]);
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
customerName: [
{
required: true,
type: 'string',
message: '请输入订单名称',
trigger: 'blur'
}
],
customerCode: [
{
required: true,
type: 'string',
message: '请输入合法的IP地址',
trigger: 'blur'
}
],
progress: [
{
required: true,
type: 'string',
message: '请选择跟进状态',
trigger: 'blur'
}
]
});
const { resetFields, validate, validateInfos } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
validate()
.then(() => {
loading.value = true;
// 去除空格
form.customerName = form.customerName?.replace(/\s*/g, '');
if(isUpdate.value == false) {
form.userId = loginUser.value.userId;
}
const data = {
...form
};
// 转字符串
const saveOrUpdate = isUpdate.value ? updateCustomer : addCustomer;
saveOrUpdate(data)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
const onUpload = (d: ItemType) => {
uploadFile(<File>d.file)
.then((result) => {
form.customerAvatar = result.path;
message.success('上传成功');
})
.catch((e) => {
message.error(e.message);
});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
loading.value = false;
// 头像赋值
images.value = [];
if(props.data.customerAvatar){
images.value.push({ uid:1, url: FILE_SERVER + props.data.customerAvatar, status: '' });
}
assignObject(form, props.data);
isUpdate.value = true;
} else {
form.customerCode = createCode();
isUpdate.value = false;
}
} else {
resetFields();
}
}
);
</script>
<style lang="less"></style>
@@ -0,0 +1,155 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
width="75%"
:visible="visible"
:confirm-loading="loading"
:title="'订单详情'"
:maxable="true"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
:footer="null"
>
<a-form
:label-col="{ md: { span: 4 }, sm: { span: 24 } }"
:wrapper-col="{ md: { span: 19 }, sm: { span: 24 } }"
>
<div class="base-form" style="margin-bottom: 20px">
<div class="card-head">
<a-avatar
:size="50"
style="margin-right: 10px"
:src="
customer.customerAvatar
? FILE_SERVER + customer.customerAvatar
: ''
"
/>
<h2>{{ customer.customerName }}</h2>
</div>
<a-descriptions bordered>
<a-descriptions-item label="订单名称">
{{ customer.customerName }}
</a-descriptions-item>
<a-descriptions-item label="订单标识">
{{ customer.customerCode }}
</a-descriptions-item>
<a-descriptions-item label="跟进状态">
<div color="blue" v-for="(d, index) in progress" :key="index">
<span v-if="d.key == customer.progress">{{ d.name }}</span>
</div>
</a-descriptions-item>
<a-descriptions-item label="座机电话">
{{ customer.customerPhone }}
</a-descriptions-item>
<a-descriptions-item label="联系人">
{{ customer.customerContacts }}
</a-descriptions-item>
<a-descriptions-item label="订单类型">
<div color="blue" v-for="(d, index) in customerType" :key="index">
<span v-if="d.key == customer.customerType">{{ d.name }}</span>
</div>
</a-descriptions-item>
<a-descriptions-item label="联系地址">
{{ customer.customerAddress }}
</a-descriptions-item>
<a-descriptions-item label="联系电话">
{{ customer.customerMobile }}
</a-descriptions-item>
<a-descriptions-item label="备注">
{{ customer.comments }}
</a-descriptions-item>
</a-descriptions>
<!-- <a-descriptions-->
<!-- title="其他信息"-->
<!-- :column="1"-->
<!-- bordered-->
<!-- style="margin-top: 30px"-->
<!-- >-->
<!-- <a-descriptions-item label="相关项目">-->
<!-- {{ customer.comments }}-->
<!-- </a-descriptions-item>-->
<!-- </a-descriptions>-->
</div>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form } from 'ant-design-vue';
import { assignObject } from 'ele-admin-pro';
import type { Customer } from '@/api/oa/customer/model';
import { FILE_SERVER } from '@/config/setting';
import { getDictionaryOptions } from '@/utils/common';
const useForm = Form.useForm;
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Customer | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 用户信息
const customer = reactive<Customer>({
customerCode: '',
customerName: '',
customerType: undefined,
customerMobile: '',
customerAvatar: '',
customerPhone: '',
customerContacts: '',
customerAddress: '',
comments: '',
progress: '',
status: '0',
sortNumber: 100,
customerId: 0
});
// 请求状态
const loading = ref(true);
const { resetFields } = useForm(customer);
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
/* 获取字典数据 */
const customerType = getDictionaryOptions('customerType');
const progress = getDictionaryOptions('customerFollowStatus');
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
loading.value = false;
assignObject(customer, props.data);
}
} else {
resetFields();
}
}
);
</script>
<style lang="less">
.tab-pane {
min-height: 100px;
}
.card-head {
display: flex;
height: 40px;
align-items: center;
margin-bottom: 30px;
}
</style>
@@ -0,0 +1,46 @@
<!-- 选择下拉框 -->
<template>
<a-select
mode="multiple"
show-search
optionFilterProp="label"
:options="data"
allow-clear
:value="value"
:placeholder="placeholder"
@update:value="updateValue"
@blur="onBlur"
/>
</template>
<script lang="ts" setup>
import { getDictionaryOptions } from '@/utils/common';
const emit = defineEmits<{
(e: 'update:value', value: string): void;
(e: 'blur'): void;
}>();
withDefaults(
defineProps<{
value?: string;
placeholder?: string;
}>(),
{
placeholder: '请选择已开发的端口'
}
);
// 字典数据
const data = getDictionaryOptions('serverOpenPort');
/* 更新选中数据 */
const updateValue = (value: string) => {
emit('update:value', value);
};
/* 失去焦点 */
const onBlur = () => {
emit('blur');
};
</script>
@@ -0,0 +1,50 @@
<!-- 选择下拉框 -->
<template>
<a-space>
<a-button type="dashed" @click="reset">全部</a-button>
<a-button
v-for="(item, index) in data"
:key="index"
type="dashed"
v-model:value.trim="where.progress"
@click="search(item.value)"
>{{ item.label }}</a-button
>
</a-space>
</template>
<script lang="ts" setup>
import { CustomerParam } from '@/api/oa/customer/model';
import useSearch from '@/utils/use-search';
import { getDictionaryOptions } from '@/utils/common';
const emit = defineEmits<{
(e: 'search', where?: CustomerParam): void;
}>();
// 字典数据
const data = getDictionaryOptions('customerFollowStatus');
// 表单数据
const { where, resetFields } = useSearch<CustomerParam>({
progress: '',
customerType: '',
customerSource: '',
betweenTime: '',
userId: ''
});
/* 搜索 */
const search = (val) => {
where.progress = val == 'all' ? null : val;
emit('search', where);
};
/* 重置 */
const reset = () => {
resetFields();
search('all');
};
</script>
<style lang="less"></style>
@@ -0,0 +1,50 @@
<!-- 选择下拉框 -->
<template>
<a-space>
<a-button type="dashed" @click="reset">全部</a-button>
<a-button
v-for="(item, index) in data"
:key="index"
type="dashed"
v-model:value.trim="where.customerSource"
@click="search(item.value)"
>{{ item.label }}</a-button
>
</a-space>
</template>
<script lang="ts" setup>
import { CustomerParam } from '@/api/oa/customer/model';
import useSearch from '@/utils/use-search';
import { getDictionaryOptions } from '@/utils/common';
const emit = defineEmits<{
(e: 'search', where?: CustomerParam): void;
}>();
// 字典数据
const data = getDictionaryOptions('customerSource');
// 表单数据
const { where, resetFields } = useSearch<CustomerParam>({
progress: '',
customerType: '',
customerSource: '',
betweenTime: '',
userId: ''
});
/* 搜索 */
const search = (val) => {
where.customerSource = val == 'all' ? null : val;
emit('search', where);
};
/* 重置 */
const reset = () => {
resetFields();
search('all');
};
</script>
<style lang="less"></style>
@@ -0,0 +1,45 @@
<!-- 选择下拉框 -->
<template>
<a-select
show-search
optionFilterProp="label"
:options="data"
allow-clear
:value="value"
:placeholder="placeholder"
@update:value="updateValue"
@blur="onBlur"
/>
</template>
<script lang="ts" setup>
import { getDictionaryOptions } from '@/utils/common';
const emit = defineEmits<{
(e: 'update:value', value: string): void;
(e: 'blur'): void;
}>();
withDefaults(
defineProps<{
// value?: string;
placeholder?: string;
}>(),
{
placeholder: '请选择状态'
}
);
// 字典数据
const data = getDictionaryOptions('status');
/* 更新选中数据 */
const updateValue = (value: string) => {
emit('update:value', value);
};
/* 失去焦点 */
const onBlur = () => {
emit('blur');
};
</script>
@@ -0,0 +1,48 @@
<!-- 选择下拉框 -->
<template>
<a-space>
<a-button type="dashed" @click="reset">全部</a-button>
<a-button
v-for="(item, index) in data"
:key="index"
type="dashed"
v-model:value.trim="where.betweenTime"
@click="search(item.value)"
>{{ item.label }}</a-button
>
</a-space>
</template>
<script lang="ts" setup>
import { CustomerParam } from '@/api/oa/customer/model';
import useSearch from '@/utils/use-search';
import { getDictionaryOptions } from '@/utils/common';
const emit = defineEmits<{
(e: 'search', where?: CustomerParam): void;
}>();
// 字典数据
const data = getDictionaryOptions('timeFilter');
// 表单数据
const { where, resetFields } = useSearch<CustomerParam>({
progress: '',
customerType: '',
customerSource: '',
betweenTime: '',
userId: ''
});
/* 搜索 */
const search = (val) => {
where.betweenTime = val == 'all' ? null : val;
emit('search', where);
};
/* 重置 */
const reset = () => {
resetFields();
search('all');
};
</script>
@@ -0,0 +1,53 @@
<!-- 选择下拉框 -->
<template>
<a-space>
<a-button type="dashed" @click="reset">全部</a-button>
<a-button
v-for="(item, index) in data"
:key="index"
type="dashed"
v-model:value.trim="where.customerType"
@click="search(item.value)"
>{{ item.label }}</a-button
>
</a-space>
</template>
<script lang="ts" setup>
import { CustomerParam } from '@/api/oa/customer/model';
import useSearch from '@/utils/use-search';
import { getDictionaryOptions } from '@/utils/common';
const emit = defineEmits<{
(e: 'search', where?: CustomerParam): void;
}>();
// 字典数据
const data = getDictionaryOptions('customerType');
// 表单数据
const { where, resetFields } = useSearch<CustomerParam>({
progress: '',
customerType: '',
customerSource: '',
betweenTime: '',
userId: ''
});
/* 搜索 */
const search = (val) => {
where.customerType = val == 'all' ? null : val;
emit('search', where);
console.log(where);
};
/* 重置 */
const reset = () => {
resetFields();
search('all');
};
// reset();
</script>
<style lang="less"></style>
@@ -0,0 +1,78 @@
<!-- 选择下拉框 -->
<template>
<a-select
show-search
optionFilterProp="label"
:options="data"
allow-clear
:placeholder="placeholder"
@change="onChange"
style="width: 300px"
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { message } from 'ant-design-vue';
import { listUsers } from '@/api/system/user';
import type { SelectProps } from 'ant-design-vue';
import { UserParam } from '@/api/system/user/model';
import { CustomerParam } from '@/api/oa/customer/model';
const emit = defineEmits<{
(e: 'search', where?: CustomerParam): void;
(e: 'update:value', value: string): void;
(e: 'blur'): void;
}>();
withDefaults(
defineProps<{
// value?: string;
placeholder?: string;
}>(),
{
placeholder: '请选择客户负责人'
}
);
// 字典数据
const data = ref<SelectProps['options']>([]);
/* 更新选中数据 */
// const updateValue = (value: string) => {
// emit('update:value', value);
// };
// 默认搜索条件
const where = ref<UserParam>({});
const search = () => {
/* 获取用户列 */
listUsers({ ...where?.value })
.then((result) => {
data.value = result?.map((d) => {
return {
value: d.userId,
label: d.nickname
};
});
})
.catch((e) => {
message.error(e.message);
});
};
const onSearch = (userId) => {
};
search();
/* 失去焦点 */
const onBlur = () => {
};
// 搜素
const onChange = (userId) => {
emit('search', { userId });
};
</script>
+140
View File
@@ -0,0 +1,140 @@
<!-- 搜索表单 -->
<template>
<a-space>
<a-button
type="primary"
class="ele-btn-icon"
v-permission="'shop:goods:save'"
@click="add"
>
<template #icon>
<PlusOutlined />
</template>
<span>新建</span>
</a-button>
<a-button
danger
type="primary"
class="ele-btn-icon"
:v-role="`dev`"
@click="removeBatch"
v-if="props.selection.length > 0"
>
<template #icon>
<DeleteOutlined />
</template>
<span>批量删除</span>
</a-button>
<a-radio-group v-model:value="listType" @change="handleTabs">
<a-radio-button value="all">全部</a-radio-button>
<a-radio-button value="onSale">出售中</a-radio-button>
<a-radio-button value="offSale">已下架</a-radio-button>
<a-radio-button value="soldOut">已售罄</a-radio-button>
</a-radio-group>
<a-input-search
allow-clear
v-model:value="searchText"
placeholder="请输入搜索关键词"
@search="search"
@pressEnter="search"
>
<template #addonBefore>
<a-select v-model:value="type" style="width: 120px; margin: -5px -12px">
<a-select-option value="goodsName">商品名称</a-select-option>
<a-select-option value="goodsCode">商品编号</a-select-option>
</a-select>
</template>
</a-input-search>
<!-- <CustomerSelect-->
<!-- v-model:value="where.goodsId"-->
<!-- :placeholder="`按商品筛选`"-->
<!-- @select="onSelect"-->
<!-- @clear="onClear"-->
<!-- />-->
</a-space>
</template>
<script lang="ts" setup>
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons-vue';
import useSearch from '@/utils/use-search';
import type { GoodsParam } from '@/api/goods/model';
// import CustomerSelect from '@/components/CustomerSelect/index.vue';
import { ref, watch } from 'vue';
const emit = defineEmits<{
(e: 'search', where?: GoodsParam): void;
(e: 'add'): void;
(e: 'remove'): void;
}>();
const props = defineProps<{
// 勾选的项目
selection?: [];
}>();
// 表单数据
const { where, resetFields } = useSearch<GoodsParam>({
goodsId: undefined,
goodsName: '',
goodsCode: ''
});
// 下来选项
const type = ref('goodsName');
// tabType
const listType = ref('all');
// 搜索内容
const searchText = ref('');
/* 搜索 */
const search = () => {
if (type.value == 'goodsName') {
where.goodsName = searchText.value;
where.goodsCode = undefined;
}
if (type.value == 'goodsCode') {
where.goodsCode = searchText.value;
where.goodsName = undefined;
}
emit('search', where);
};
const handleTabs = (e) => {
listType.value = e.target.value;
if (listType.value == 'all') {
resetFields();
}
if (listType.value == 'onSale') {
where.status = 0;
}
if (listType.value == 'offSale') {
where.status = 1;
}
if (listType.value == 'soldOut') {
where.stockTotal = 0;
}
emit('search', where);
};
// 新增
const add = () => {
emit('add');
};
// 批量删除
const removeBatch = () => {
emit('remove');
};
/* 重置 */
// const reset = () => {
// resetFields();
// search();
// };
// 监听字典id变化
watch(
() => props.selection,
() => {}
);
</script>
@@ -0,0 +1,45 @@
<!-- 角色选择下拉框 -->
<template>
<a-select
show-search
optionFilterProp="label"
:options="data"
allow-clear
:value="value"
:placeholder="placeholder"
@update:value="updateValue"
@blur="onBlur"
/>
</template>
<script lang="ts" setup>
import { getDictionaryOptions } from '@/utils/common';
const emit = defineEmits<{
(e: 'update:value', value: string): void;
(e: 'blur'): void;
}>();
withDefaults(
defineProps<{
value?: string;
placeholder?: string;
}>(),
{
placeholder: '请选择年限'
}
);
// 字典数据
const data = getDictionaryOptions('serverYears');
/* 更新选中数据 */
const updateValue = (value: string) => {
emit('update:value', value);
};
/* 失去焦点 */
const onBlur = () => {
emit('blur');
};
</script>
+345
View File
@@ -0,0 +1,345 @@
<template>
<div class="page">
<!-- <a-page-header :ghost="false" title="商品管理">-->
<!-- <div class="ele-text-secondary">-->
<!-- 商品管理模块,支持视频上传、多图上传、多规格。-->
<!-- </div>-->
<!-- </a-page-header>-->
<div class="ele-body">
<a-card :bordered="false">
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="goodsId"
:columns="columns"
:height="tableHeight"
:datasource="datasource"
v-model:selection="selection"
:scroll="{ x: 800 }"
>
<template #toolbar>
<search
:selection="selection"
@search="reload"
@add="openEdit"
@remove="removeBatch"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image :src="record.image" :preview="false" :width="80" />
</template>
<template v-if="column.key === 'goodsName'">
<a-tooltip :title="record.goodsName">
<p class="twoline-hide">
{{ record.goodsName }}
</p>
</a-tooltip>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">上架</a-tag>
<a-tag v-if="record.status === 1" color="orange">下架</a-tag>
</template>
<template v-if="column.key === 'comments'">
<a-tooltip :title="record.comments" placement="topLeft">
{{ record.comments }}
</a-tooltip>
</template>
<template v-if="column.key === 'createTime'">
<a-tooltip :title="`${toDateString(record.createTime)}`">
{{ timeAgo(record.createTime) }}
</a-tooltip>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<goods-edit
v-model:visible="showEdit"
:data="current"
:category-list="data"
@done="reload"
/>
<!-- 用户详情 -->
<goods-info v-model:visible="showInfo" :data="current" @done="reload" />
</div>
</div>
</template>
<!--suppress TypeScriptValidateTypes -->
<script lang="ts" setup>
import { timeAgo, toTreeData } from 'ele-admin-pro';
import { createVNode, computed, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import { toDateString } from 'ele-admin-pro';
import Search from './components/search.vue';
import GoodsEdit from './components/goods-edit.vue';
import GoodsInfo from './components/goods-info.vue';
import { pageGoods, removeGoods, removeBatchGoods } from '@/api/goods';
import type { Goods, GoodsParam } from '@/api/goods/model';
import { listCategory } from '@/api/goods/category';
import { Category } from '@/api/goods/category/model';
import { FILE_THUMBNAIL } from '@/config/setting';
// import { getDictionaryOptions } from '@/utils/common';
// import { useUserStore } from '@/store/modules/user';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 是否显示资产详情
const showInfo = ref(false);
// 加载状态
const loading = ref(true);
// 树展开的key
const expandedRowKeys = ref<number[]>([]);
// 树选中的key
const selectedRowKeys = ref<number[]>([]);
// 当前用户信息
// const brand = getDictionaryOptions('serverBrand');
// 表格列配置
const columns = ref<ColumnItem[]>([
{
key: 'index',
width: 48,
align: 'center',
fixed: 'left',
hideInSetting: true,
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
},
{
title: '商品图片',
dataIndex: 'image',
width: 110,
key: 'image'
},
{
title: '商品名称',
dataIndex: 'goodsName',
key: 'goodsName',
width: 320,
ellipsis: true
},
{
title: '商品分类',
dataIndex: 'categoryName'
},
{
title: '商品价格',
dataIndex: 'goodsPriceMin',
key: 'goodsPriceMin'
},
{
title: '总销量',
dataIndex: 'salesActual',
key: 'salesActual'
},
{
title: '库存',
dataIndex: 'stockTotal',
key: 'stockTotal'
},
{
title: '备注',
dataIndex: 'comments',
key: 'comments',
hideInTable: true,
ellipsis: true
},
{
title: '状态',
dataIndex: 'status',
sorter: true,
key: 'status',
showSorterTooltip: false,
customRender: ({ text }) => ['上架', '下架'][text]
},
{
title: '排序',
dataIndex: 'sortNumber',
sorter: true
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
sorter: true,
ellipsis: true,
hideInTable: true,
customRender: ({ text }) => toDateString(text)
},
{
title: '操作',
key: 'action',
width: 200,
align: 'center',
hideInSetting: true
}
]);
// 表格选中数据
const selection = ref<Goods[]>([]);
// 当前编辑数据
const current = ref<Goods | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 树形数据
const data = ref<Category[]>([]);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.brand = filters.brand;
}
return pageGoods({ ...where, ...orders, page, limit });
};
/* 查询 */
const query = () => {
loading.value = true;
listCategory()
.then((list) => {
loading.value = false;
const eks: number[] = [];
list.forEach((d) => {
d.key = d.categoryId;
d.value = d.categoryId;
if (typeof d.categoryId === 'number') {
eks.push(d.categoryId);
}
});
expandedRowKeys.value = eks;
data.value = toTreeData({
data: list,
idField: 'categoryId',
parentIdField: 'parentId'
});
if (list.length) {
if (typeof list[0].categoryId === 'number') {
selectedRowKeys.value = [list[0].categoryId];
}
current.value = list[0];
} else {
selectedRowKeys.value = [];
current.value = null;
}
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
};
/* 搜索 */
const reload = (where?: GoodsParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
// 搜索是否展开
const searchExpand = ref(false);
// 表格固定高度
const fixedHeight = ref(false);
// 表格高度
const tableHeight = computed(() => {
return fixedHeight.value
? searchExpand.value
? 'calc(100vh - 618px)'
: 'calc(100vh - 562px)'
: void 0;
});
/* 打开编辑弹窗 */
const openEdit = (row?: Goods) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 删除单个 */
const remove = (row: Goods) => {
const hide = message.loading('请求中..', 0);
removeGoods(row.goodsId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchGoods(selection.value.map((d) => d.goodsId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
query();
</script>
<script lang="ts">
export default {
name: 'GoodsIndex'
};
</script>
<style lang="less" scoped>
// 文字超出隐藏(两行)
// 需要设置文字容器的宽度
.twoline-hide {
width: 320px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
white-space: normal;
}
</style>
@@ -0,0 +1,181 @@
<!-- 服务编辑弹窗 -->
<template>
<ele-modal
:width="500"
:visible="visible"
:confirm-loading="loading"
:title="isUpdate ? '修改记录' : '添加记录'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 5, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="名称" name="name">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入服务承诺名称"
v-model:value="form.name"
/>
</a-form-item>
<a-form-item label="描述" name="summary">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入描述内容"
v-model:value="form.summary"
/>
</a-form-item>
<a-form-item label="排序" name="sortNumber">
<a-input
allow-clear
:maxlength="20"
placeholder="排序"
v-model:value="form.sortNumber"
/>
</a-form-item>
<a-form-item label="默认勾选" name="isDefault">
<a-radio-group v-model:value="form.isDefault">
<a-radio :value="1">是</a-radio>
<a-radio :value="0">否</a-radio>
</a-radio-group>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { message } from 'ant-design-vue/es';
import type { FormInstance, Rule } from 'ant-design-vue/es/form';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import { addService, updateService } from '@/api/goods/service/index';
import type { Service } from '@/api/goods/service/model';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Service | null;
}>();
//
const formRef = ref<FormInstance | null>(null);
// 是否是修改
const isUpdate = ref(false);
// 提交状态
const loading = ref(false);
// 表单数据
const { form, resetFields, assignFields } = useFormData<Service>({
serviceId: undefined,
name: '',
summary: '',
isDefault: 1,
sortNumber: 100
});
// 表单验证规则
const rules = reactive<Record<string, Rule[]>>({
name: [
{
required: true,
message: '请输入服务承诺名称',
type: 'string',
trigger: 'blur'
}
],
summary: [
{
required: true,
message: '请输入服务承诺描述',
type: 'string',
trigger: 'blur'
}
],
isDefault: [
{
required: true,
message: '请设置是否勾选',
type: 'number',
trigger: 'blur'
}
],
sortNumber: [
{
required: true,
message: '请填写排序',
type: 'number',
trigger: 'blur'
}
]
});
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const saveOrUpdate = isUpdate.value ? updateService : addService;
saveOrUpdate(form)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
assignFields(props.data);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
formRef.value?.clearValidate();
}
}
);
</script>
+212
View File
@@ -0,0 +1,212 @@
<template>
<div class="ele-body">
<a-card :bordered="false">
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="serviceId"
:columns="columns"
:datasource="datasource"
:scroll="{ x: 800 }"
cache-key="proShopServiceTable"
>
<template #toolbar>
<a-space>
<a-button type="primary" class="ele-btn-icon" @click="openEdit()">
<template #icon>
<plus-outlined />
</template>
<span>新建</span>
</a-button>
<a-input-search
allow-clear
v-model:value="searchText"
placeholder="请输入关键词"
@search="reload"
@pressEnter="reload"
/>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'isDefault'">
<!-- 是否默认 -->
<a-tag :color="record.isDefault ? 'green' : ''">
{{ record.isDefault ? '是' : '否' }}
</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
placement="topRight"
title="确定要删除此服务承诺吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<service-edit v-model:visible="showEdit" :data="current" @done="reload" />
</div>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue/es';
import {
PlusOutlined,
DeleteOutlined,
ExclamationCircleOutlined
} from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro/es';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import { messageLoading, toDateString } from 'ele-admin-pro/es';
import ServiceEdit from './components/service-edit.vue';
import {
pageServices,
removeService,
removeServices
} from '@/api/goods/service';
import type { Service, ServiceParam } from '@/api/goods/service/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
key: 'index',
width: 48,
align: 'center',
fixed: 'left',
hideInSetting: true,
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
},
{
title: '服务名称',
dataIndex: 'name',
showSorterTooltip: false
},
{
title: '描述',
dataIndex: 'summary',
showSorterTooltip: false
},
{
title: '是否默认',
dataIndex: 'isDefault',
key: 'isDefault',
width: 180
},
{
title: '创建时间',
dataIndex: 'createTime',
width: 180,
customRender: ({ text }) => toDateString(text)
},
{
title: '操作',
key: 'action',
width: 200,
align: 'center'
}
]);
// 表格选中数据
const selection = ref<Service[]>([]);
// 当前编辑数据
const current = ref<Service | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
const type = ref('goodsName');
const searchText = ref('');
const score = ref(0);
const handleTabs = (e) => {
score.value = e.target.value;
};
// 表格数据源
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
if (type.value == 'goodsName') {
where.goodsName = searchText.value;
}
if (type.value == 'goodsCode') {
where.goodsCode = searchText.value;
}
if (type.value == 'userId') {
where.userId = searchText.value;
}
return pageServices({ ...where, ...orders, page, limit });
};
/* 搜索 */
const reload = (where?: ServiceParam) => {
selection.value = [];
tableRef?.value?.reload({ page: 1, where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: Service) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 删除单个 */
const remove = (row: Service) => {
const hide = messageLoading('请求中..', 0);
removeService(row.serviceId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的服务承诺吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = messageLoading('请求中..', 0);
removeServices(selection.value.map((d) => d.serviceId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
</script>
<script lang="ts">
export default {
name: 'ShopService'
};
</script>