1. 推广员功能增强
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CI / CI OK (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CI / CI OK (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
This commit is contained in:
@@ -96,6 +96,7 @@ export async function sendVerificationCode(params: {
|
||||
if (params.account) {
|
||||
return requestClient.post<AuthApi.SendCodeResult>('send-verification-code', {
|
||||
account: params.account,
|
||||
password: params.password,
|
||||
admin_id: params.admin_id,
|
||||
});
|
||||
}
|
||||
|
||||
13
apps/web-antd/src/api/order/priceAdjust.ts
Normal file
13
apps/web-antd/src/api/order/priceAdjust.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
const orderPrefix = 'order/';
|
||||
|
||||
export async function adjustOrderPercent(data: Record<string, any>) {
|
||||
return requestClient.post(`${orderPrefix}adjust-order-percent`, data);
|
||||
}
|
||||
|
||||
export async function getOrderPriceAdjustConfig(storeId?: number) {
|
||||
return requestClient.get(`${orderPrefix}price-adjust-config`, {
|
||||
params: storeId ? { store_id: storeId } : {},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
|
||||
|
||||
|
||||
import { useDebounceFn, useVModel } from '@vueuse/core';
|
||||
|
||||
import { Avatar, Empty, Input, Popover, Spin, Tabs, Tag } from 'ant-design-vue';
|
||||
|
||||
|
||||
|
||||
import { searchApiLogUsers } from '#/views/log/api-log/api';
|
||||
|
||||
|
||||
|
||||
defineOptions({
|
||||
|
||||
name: 'ApiLogUserPicker',
|
||||
|
||||
inheritAttrs: false,
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
export type ApiLogUserFilter = {
|
||||
|
||||
id: number;
|
||||
|
||||
user_type: 'admin' | 'patient' | 'doctor';
|
||||
|
||||
platform_type: 0 | 1 | 2;
|
||||
|
||||
name: string;
|
||||
|
||||
role_label: string;
|
||||
|
||||
avatar?: string;
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
type ApiLogUserOption = ApiLogUserFilter;
|
||||
|
||||
|
||||
|
||||
type UserTab = 'admin' | 'patient' | 'doctor';
|
||||
|
||||
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
value?: ApiLogUserFilter;
|
||||
|
||||
disabled?: boolean;
|
||||
|
||||
onPlatformTypeChange?: (platformType: 0 | 1 | 2) => void;
|
||||
|
||||
}>();
|
||||
|
||||
|
||||
|
||||
const emits = defineEmits<{
|
||||
|
||||
'update:value': [value: ApiLogUserFilter | undefined];
|
||||
|
||||
}>();
|
||||
|
||||
|
||||
|
||||
const mValue = useVModel(props, 'value', emits, { passive: true });
|
||||
|
||||
|
||||
|
||||
const open = ref(false);
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const searchKeyword = ref('');
|
||||
|
||||
const activeTab = ref<UserTab>('admin');
|
||||
|
||||
const options = ref<ApiLogUserOption[]>([]);
|
||||
|
||||
const searchInputRef = ref<InstanceType<typeof Input> | null>(null);
|
||||
|
||||
|
||||
|
||||
const TAB_ITEMS = [
|
||||
|
||||
{ key: 'admin', label: '后台用户' },
|
||||
|
||||
{ key: 'patient', label: '患者端用户' },
|
||||
|
||||
{ key: 'doctor', label: '医生端用户' },
|
||||
|
||||
] as const;
|
||||
|
||||
|
||||
|
||||
const searchPlaceholder = computed(() => {
|
||||
|
||||
if (activeTab.value === 'admin') return '搜索手机号或昵称';
|
||||
|
||||
if (activeTab.value === 'patient') return '搜索用户名字';
|
||||
|
||||
return '搜索名字或手机号';
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
async function fetchOptions() {
|
||||
|
||||
const keyword = searchKeyword.value.trim();
|
||||
|
||||
if (!keyword) {
|
||||
|
||||
options.value = [];
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
|
||||
const result = await searchApiLogUsers({
|
||||
|
||||
user_type: activeTab.value,
|
||||
|
||||
keyword,
|
||||
|
||||
pageSize: 24,
|
||||
|
||||
});
|
||||
|
||||
options.value = result?.items ?? [];
|
||||
|
||||
} finally {
|
||||
|
||||
loading.value = false;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const debouncedFetch = useDebounceFn(fetchOptions, 300);
|
||||
|
||||
|
||||
|
||||
watch([searchKeyword, activeTab], () => {
|
||||
|
||||
debouncedFetch();
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
function selectOption(item: ApiLogUserOption) {
|
||||
|
||||
mValue.value = {
|
||||
|
||||
id: item.id,
|
||||
|
||||
user_type: item.user_type,
|
||||
|
||||
platform_type: item.platform_type,
|
||||
|
||||
name: item.name,
|
||||
|
||||
role_label: item.role_label,
|
||||
|
||||
avatar: item.avatar,
|
||||
|
||||
};
|
||||
|
||||
props.onPlatformTypeChange?.(item.platform_type);
|
||||
|
||||
open.value = false;
|
||||
|
||||
searchKeyword.value = '';
|
||||
|
||||
options.value = [];
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
function clearSelection(e: Event) {
|
||||
|
||||
e.stopPropagation();
|
||||
|
||||
mValue.value = undefined;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
function onOpenChange(next: boolean) {
|
||||
|
||||
if (props.disabled) return;
|
||||
|
||||
open.value = next;
|
||||
|
||||
if (next) {
|
||||
|
||||
searchKeyword.value = '';
|
||||
|
||||
options.value = [];
|
||||
|
||||
nextTick(() => searchInputRef.value?.focus?.());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
watch(
|
||||
|
||||
() => props.disabled,
|
||||
|
||||
(disabled) => {
|
||||
|
||||
if (disabled) open.value = false;
|
||||
|
||||
},
|
||||
|
||||
);
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<template>
|
||||
|
||||
<Popover
|
||||
|
||||
:open="open"
|
||||
|
||||
trigger="click"
|
||||
|
||||
placement="bottomLeft"
|
||||
|
||||
overlay-class-name="api-log-user-picker-popover"
|
||||
|
||||
@open-change="onOpenChange"
|
||||
|
||||
>
|
||||
|
||||
<template #content>
|
||||
|
||||
<div class="api-log-user-panel">
|
||||
|
||||
<Tabs v-model:active-key="activeTab" size="small" class="api-log-user-tabs">
|
||||
|
||||
<Tabs.TabPane
|
||||
|
||||
v-for="tab in TAB_ITEMS"
|
||||
|
||||
:key="tab.key"
|
||||
|
||||
:tab="tab.label"
|
||||
|
||||
/>
|
||||
|
||||
</Tabs>
|
||||
|
||||
<Input
|
||||
|
||||
ref="searchInputRef"
|
||||
|
||||
v-model:value="searchKeyword"
|
||||
|
||||
allow-clear
|
||||
|
||||
:placeholder="searchPlaceholder"
|
||||
|
||||
class="api-log-user-search"
|
||||
|
||||
/>
|
||||
|
||||
<Spin :spinning="loading">
|
||||
|
||||
<div v-if="options.length" class="api-log-user-grid">
|
||||
|
||||
<button
|
||||
|
||||
v-for="item in options"
|
||||
|
||||
:key="`${item.user_type}-${item.id}`"
|
||||
|
||||
type="button"
|
||||
|
||||
class="api-log-user-card"
|
||||
|
||||
:class="{
|
||||
|
||||
active:
|
||||
|
||||
mValue?.id === item.id && mValue?.user_type === item.user_type,
|
||||
|
||||
}"
|
||||
|
||||
@click="selectOption(item)"
|
||||
|
||||
>
|
||||
|
||||
<Avatar :src="item.avatar" :size="30">
|
||||
|
||||
{{ (item.name || '?').charAt(0) }}
|
||||
|
||||
</Avatar>
|
||||
|
||||
<div class="api-log-user-card-id">#{{ item.id }}</div>
|
||||
|
||||
<div class="api-log-user-card-name" :title="item.name">
|
||||
|
||||
{{ item.name }}
|
||||
|
||||
</div>
|
||||
|
||||
<Tag class="api-log-user-card-role" :bordered="false">
|
||||
|
||||
{{ item.role_label }}
|
||||
|
||||
</Tag>
|
||||
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<Empty
|
||||
|
||||
v-else
|
||||
|
||||
:description="searchKeyword.trim() ? '无匹配用户' : '请输入关键词搜索'"
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Empty, Input, Popover, Spin } from 'ant-design-vue';
|
||||
|
||||
import { getExpressCompaniesOption } from '#/views/business/express/express-company/api';
|
||||
|
||||
defineOptions({
|
||||
name: 'ExpressCompanySelect',
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const props = defineProps<{
|
||||
value?: string;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
}>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
'update:value': [value: string | undefined];
|
||||
}>();
|
||||
|
||||
const mValue = useVModel(props, 'value', emits, { passive: true });
|
||||
|
||||
type ExpressCompanyOption = {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
};
|
||||
|
||||
const open = ref(false);
|
||||
const loading = ref(false);
|
||||
const searchKeyword = ref('');
|
||||
const options = ref<ExpressCompanyOption[]>([]);
|
||||
const searchInputRef = ref<InstanceType<typeof Input> | null>(null);
|
||||
|
||||
function filterExpressCompanyOptions(keyword: string, list: ExpressCompanyOption[]) {
|
||||
const q = keyword.trim().toLowerCase();
|
||||
if (!q) return list;
|
||||
return list.filter((item) => {
|
||||
const name = (item.name || '').toLowerCase();
|
||||
const code = (item.code || '').toLowerCase();
|
||||
return name.includes(q) || code.includes(q);
|
||||
});
|
||||
}
|
||||
|
||||
const filteredOptions = computed(() =>
|
||||
filterExpressCompanyOptions(searchKeyword.value, options.value),
|
||||
);
|
||||
|
||||
const selectedOption = computed(() =>
|
||||
options.value.find((item) => item.code === mValue.value),
|
||||
);
|
||||
|
||||
const displayText = computed(() => {
|
||||
const item = selectedOption.value;
|
||||
if (!item) return '';
|
||||
return item.code ? `${item.name || '-'}(${item.code})` : (item.name || '-');
|
||||
});
|
||||
|
||||
async function loadOptions() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getExpressCompaniesOption({});
|
||||
options.value = Array.isArray(res) ? res : [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function selectOption(item: ExpressCompanyOption) {
|
||||
mValue.value = item.code;
|
||||
open.value = false;
|
||||
searchKeyword.value = '';
|
||||
}
|
||||
|
||||
function clearSelection(e: Event) {
|
||||
e.stopPropagation();
|
||||
mValue.value = undefined;
|
||||
}
|
||||
|
||||
function onOpenChange(next: boolean) {
|
||||
if (props.disabled) return;
|
||||
open.value = next;
|
||||
if (next) {
|
||||
searchKeyword.value = '';
|
||||
if (!options.value.length) {
|
||||
loadOptions();
|
||||
}
|
||||
nextTick(() => searchInputRef.value?.focus?.());
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.disabled,
|
||||
(disabled) => {
|
||||
if (disabled) open.value = false;
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
loadOptions();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Popover
|
||||
:open="open"
|
||||
trigger="click"
|
||||
placement="bottomLeft"
|
||||
overlay-class-name="express-company-select-popover"
|
||||
@open-change="onOpenChange"
|
||||
>
|
||||
<template #content>
|
||||
<div class="express-panel">
|
||||
<Input
|
||||
ref="searchInputRef"
|
||||
v-model:value="searchKeyword"
|
||||
allow-clear
|
||||
placeholder="搜索公司名称或编码"
|
||||
class="express-search"
|
||||
/>
|
||||
<Spin :spinning="loading">
|
||||
<div v-if="filteredOptions.length" class="express-list">
|
||||
<button
|
||||
v-for="item in filteredOptions"
|
||||
:key="item.id"
|
||||
type="button"
|
||||
class="express-item"
|
||||
:class="{ active: item.code === mValue }"
|
||||
@click="selectOption(item)"
|
||||
>
|
||||
<div class="express-meta">
|
||||
<div class="express-name">{{ item.name || '-' }}</div>
|
||||
<div class="express-sub">{{ item.code }}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<Empty v-else description="无匹配快递公司" class="express-empty" />
|
||||
</Spin>
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
class="express-trigger"
|
||||
:class="{ disabled: disabled, placeholder: !displayText }"
|
||||
>
|
||||
<Input
|
||||
:value="displayText"
|
||||
readonly
|
||||
:disabled="disabled"
|
||||
:placeholder="placeholder || '请选择快递公司'"
|
||||
class="express-trigger-input"
|
||||
>
|
||||
<template v-if="displayText && !disabled" #suffix>
|
||||
<span class="clear-btn" @click="clearSelection">×</span>
|
||||
</template>
|
||||
</Input>
|
||||
</div>
|
||||
</Popover>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.express-trigger {
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
.express-trigger.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.express-trigger-input {
|
||||
pointer-events: none;
|
||||
}
|
||||
.express-trigger:not(.disabled) :deep(.ant-input) {
|
||||
cursor: pointer;
|
||||
}
|
||||
.clear-btn {
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
color: #86909c;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.express-panel {
|
||||
width: 360px;
|
||||
max-width: 80vw;
|
||||
}
|
||||
.express-search {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.express-list {
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.express-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.express-item:hover,
|
||||
.express-item.active {
|
||||
background: #f2f3f5;
|
||||
}
|
||||
.express-meta {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.express-name {
|
||||
font-size: 14px;
|
||||
color: #1d2129;
|
||||
}
|
||||
.express-sub {
|
||||
font-size: 12px;
|
||||
color: #86909c;
|
||||
}
|
||||
.express-empty {
|
||||
margin: 12px 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.express-company-select-popover .ant-popover-inner {
|
||||
padding: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,143 @@
|
||||
// Shared picker layout mixins — property-only dark variants, no global class selectors.
|
||||
|
||||
@mixin picker-card-base {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 10px 6px;
|
||||
border: 1px solid #e5e6eb;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
min-width: 0;
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
background-color 0.2s;
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
border-color: #165dff;
|
||||
background: #f2f7ff;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin picker-card-name {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
color: #1d2129;
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@mixin picker-card-sub {
|
||||
font-size: 11px;
|
||||
color: #86909c;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
@mixin picker-selected-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e5e6eb;
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #165dff;
|
||||
background: #f2f7ff;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin picker-trigger-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
padding: 8px 12px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 8px;
|
||||
color: #86909c;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
color 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #165dff;
|
||||
color: #165dff;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin picker-clear-btn {
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
color: #86909c;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
padding: 0 4px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
color: #1d2129;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin picker-card-dark-props {
|
||||
background: #1f2937;
|
||||
border-color: #374151;
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
background: #374151;
|
||||
border-color: #60a5fa;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin picker-selected-card-dark-props {
|
||||
background: #1f2937;
|
||||
border-color: #374151;
|
||||
|
||||
&:hover {
|
||||
background: #374151;
|
||||
border-color: #60a5fa;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin picker-text-primary-dark {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
@mixin picker-text-secondary-dark {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
@mixin picker-placeholder-dark-props {
|
||||
border-color: #4b5563;
|
||||
color: #9ca3af;
|
||||
|
||||
&:hover {
|
||||
border-color: #60a5fa;
|
||||
color: #60a5fa;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin picker-clear-dark-props {
|
||||
color: #9ca3af;
|
||||
|
||||
&:hover {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,11 @@
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Avatar, Empty, Input, Popover, Spin } from 'ant-design-vue';
|
||||
import { Avatar, Empty, Input, Popover, Spin, Tag } from 'ant-design-vue';
|
||||
|
||||
import PromoterInfoCard from '#/components/promoter/PromoterInfoCard.vue';
|
||||
import { getPromoterOptions } from '#/views/system/store-input/api';
|
||||
import { resolveAvatarUrl } from '#/views/system/store-input/utils/inputUser';
|
||||
|
||||
defineOptions({
|
||||
name: 'PromoterPicker',
|
||||
@@ -27,6 +29,7 @@ type PromoterOption = {
|
||||
nick_name: string;
|
||||
avatar: string;
|
||||
code: string;
|
||||
phone: string;
|
||||
role_id: number;
|
||||
role_name: string;
|
||||
};
|
||||
@@ -35,7 +38,6 @@ const open = ref(false);
|
||||
const loading = ref(false);
|
||||
const searchKeyword = ref('');
|
||||
const options = ref<PromoterOption[]>([]);
|
||||
const triggerRef = ref<HTMLElement | null>(null);
|
||||
const searchInputRef = ref<InstanceType<typeof Input> | null>(null);
|
||||
|
||||
function filterPromoterOptions(keyword: string, list: PromoterOption[]) {
|
||||
@@ -44,8 +46,14 @@ function filterPromoterOptions(keyword: string, list: PromoterOption[]) {
|
||||
return list.filter((item) => {
|
||||
const name = (item.nick_name || '').toLowerCase();
|
||||
const code = (item.code || '').toLowerCase();
|
||||
const phone = (item.phone || '').toLowerCase();
|
||||
const role = (item.role_name || '').toLowerCase();
|
||||
return name.includes(q) || code.includes(q) || role.includes(q);
|
||||
return (
|
||||
name.includes(q) ||
|
||||
code.includes(q) ||
|
||||
phone.includes(q) ||
|
||||
role.includes(q)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -57,12 +65,6 @@ const selectedOption = computed(() =>
|
||||
options.value.find((item) => item.code === mValue.value),
|
||||
);
|
||||
|
||||
const displayText = computed(() => {
|
||||
const item = selectedOption.value;
|
||||
if (!item) return '';
|
||||
return `${item.nick_name || '-'} · ${item.code} · ${item.role_name || ''}`;
|
||||
});
|
||||
|
||||
async function loadOptions() {
|
||||
loading.value = true;
|
||||
try {
|
||||
@@ -81,8 +83,7 @@ function selectOption(item: PromoterOption) {
|
||||
searchKeyword.value = '';
|
||||
}
|
||||
|
||||
function clearSelection(e: Event) {
|
||||
e.stopPropagation();
|
||||
function clearSelection() {
|
||||
mValue.value = undefined;
|
||||
}
|
||||
|
||||
@@ -124,120 +125,144 @@ onMounted(() => {
|
||||
ref="searchInputRef"
|
||||
v-model:value="searchKeyword"
|
||||
allow-clear
|
||||
placeholder="搜索业务员姓名"
|
||||
placeholder="搜索业务员姓名、业务码、手机号"
|
||||
class="promoter-search"
|
||||
/>
|
||||
<Spin :spinning="loading">
|
||||
<div v-if="filteredOptions.length" class="promoter-list">
|
||||
<div v-if="filteredOptions.length" class="promoter-grid">
|
||||
<button
|
||||
v-for="item in filteredOptions"
|
||||
:key="item.id"
|
||||
type="button"
|
||||
class="promoter-item"
|
||||
class="promoter-card"
|
||||
:class="{ active: item.code === mValue }"
|
||||
@click="selectOption(item)"
|
||||
>
|
||||
<Avatar :src="item.avatar" :size="32">
|
||||
<Avatar :src="resolveAvatarUrl(item.avatar)" :size="30">
|
||||
{{ (item.nick_name || '?').charAt(0) }}
|
||||
</Avatar>
|
||||
<div class="promoter-meta">
|
||||
<div class="promoter-name">{{ item.nick_name || '-' }}</div>
|
||||
<div class="promoter-sub">{{ item.code }} · {{ item.role_name }}</div>
|
||||
<div class="promoter-card-name" :title="item.nick_name">
|
||||
{{ item.nick_name || '-' }}
|
||||
</div>
|
||||
<div class="promoter-card-sub">{{ item.code }}</div>
|
||||
<div v-if="item.phone" class="promoter-card-phone">{{ item.phone }}</div>
|
||||
<Tag class="promoter-card-role" :bordered="false">
|
||||
{{ item.role_name || '业务员' }}
|
||||
</Tag>
|
||||
</button>
|
||||
</div>
|
||||
<Empty v-else description="无匹配业务员" class="promoter-empty" />
|
||||
</Spin>
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
ref="triggerRef"
|
||||
class="promoter-trigger"
|
||||
:class="{ disabled: disabled, placeholder: !displayText }"
|
||||
>
|
||||
<Input
|
||||
:value="displayText"
|
||||
readonly
|
||||
<div class="promoter-trigger" :class="{ disabled: disabled }">
|
||||
<PromoterInfoCard
|
||||
v-if="selectedOption"
|
||||
:admin="selectedOption"
|
||||
size="trigger"
|
||||
:show-clear="!disabled"
|
||||
:disabled="disabled"
|
||||
placeholder="请选择业务员"
|
||||
class="promoter-trigger-input"
|
||||
>
|
||||
<template v-if="displayText && !disabled" #suffix>
|
||||
<span class="clear-btn" @click="clearSelection">×</span>
|
||||
</template>
|
||||
</Input>
|
||||
:clickable="false"
|
||||
@clear="clearSelection"
|
||||
/>
|
||||
<div v-else class="promoter-trigger-placeholder">请选择业务员</div>
|
||||
</div>
|
||||
</Popover>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
<style scoped lang="scss">
|
||||
@use './picker-card-theme.scss' as theme;
|
||||
|
||||
.promoter-trigger {
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.promoter-trigger.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.65;
|
||||
}
|
||||
.promoter-trigger-input {
|
||||
pointer-events: none;
|
||||
}
|
||||
.promoter-trigger:not(.disabled) :deep(.ant-input) {
|
||||
|
||||
.promoter-trigger:not(.disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
.clear-btn {
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
color: #86909c;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
padding: 0 4px;
|
||||
|
||||
.promoter-trigger-placeholder {
|
||||
@include theme.picker-trigger-placeholder;
|
||||
}
|
||||
.promoter-panel {
|
||||
width: 360px;
|
||||
max-width: 80vw;
|
||||
}
|
||||
.promoter-search {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.promoter-list {
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.promoter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.promoter-item:hover,
|
||||
.promoter-item.active {
|
||||
background: #f2f3f5;
|
||||
}
|
||||
.promoter-meta {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.promoter-name {
|
||||
font-size: 14px;
|
||||
color: #1d2129;
|
||||
}
|
||||
.promoter-sub {
|
||||
font-size: 12px;
|
||||
color: #86909c;
|
||||
}
|
||||
.promoter-empty {
|
||||
margin: 12px 0;
|
||||
|
||||
.dark {
|
||||
.promoter-trigger-placeholder {
|
||||
@include theme.picker-placeholder-dark-props;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.promoter-picker-popover .ant-popover-inner {
|
||||
padding: 12px;
|
||||
<style lang="scss">
|
||||
@use './picker-card-theme.scss' as theme;
|
||||
|
||||
.promoter-picker-popover {
|
||||
.ant-popover-inner {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.promoter-panel {
|
||||
width: 540px;
|
||||
max-width: 86vw;
|
||||
}
|
||||
|
||||
.promoter-search {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.promoter-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(118px, 1fr));
|
||||
gap: 8px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.promoter-card {
|
||||
@include theme.picker-card-base;
|
||||
}
|
||||
|
||||
.promoter-card-name {
|
||||
@include theme.picker-card-name;
|
||||
}
|
||||
|
||||
.promoter-card-sub,
|
||||
.promoter-card-phone {
|
||||
@include theme.picker-card-sub;
|
||||
}
|
||||
|
||||
.promoter-card-role {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
padding: 0 6px;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.promoter-empty {
|
||||
margin: 16px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dark .promoter-picker-popover {
|
||||
.promoter-card {
|
||||
@include theme.picker-card-dark-props;
|
||||
}
|
||||
|
||||
.promoter-card-name {
|
||||
@include theme.picker-text-primary-dark;
|
||||
}
|
||||
|
||||
.promoter-card-sub,
|
||||
.promoter-card-phone {
|
||||
@include theme.picker-text-secondary-dark;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
109
apps/web-antd/src/components/promoter/PromoterDetailModal.vue
Normal file
109
apps/web-antd/src/components/promoter/PromoterDetailModal.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Avatar, Descriptions, Spin } from 'ant-design-vue';
|
||||
|
||||
import { getPromoterDetailApi } from '#/views/system/store-input/api';
|
||||
import { resolveAvatarUrl } from '#/views/system/store-input/utils/inputUser';
|
||||
|
||||
type PromoterDetail = {
|
||||
id: number;
|
||||
nick_name: string;
|
||||
avatar: string;
|
||||
code: string;
|
||||
phone: string;
|
||||
role_name: string;
|
||||
clinic_input_count: number;
|
||||
pharmacy_input_count: number;
|
||||
};
|
||||
|
||||
const loading = ref(false);
|
||||
const detail = ref<PromoterDetail | null>(null);
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
title: '业务员详情',
|
||||
class: 'w-[480px]',
|
||||
footer: false,
|
||||
onOpenChange(isOpen) {
|
||||
if (!isOpen) {
|
||||
detail.value = null;
|
||||
return;
|
||||
}
|
||||
const data = modalApi.getData<{ id?: number; code?: string }>();
|
||||
loadDetail(data?.id, data?.code);
|
||||
},
|
||||
});
|
||||
|
||||
async function loadDetail(id?: number, code?: string) {
|
||||
loading.value = true;
|
||||
try {
|
||||
detail.value = await getPromoterDetailApi({ id, code });
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal>
|
||||
<Spin :spinning="loading">
|
||||
<div v-if="detail" class="promoter-detail">
|
||||
<div class="promoter-detail__header">
|
||||
<Avatar
|
||||
v-if="resolveAvatarUrl(detail.avatar)"
|
||||
:src="resolveAvatarUrl(detail.avatar)"
|
||||
:size="64"
|
||||
/>
|
||||
<Avatar v-else :size="64">{{ (detail.nick_name || '?').charAt(0) }}</Avatar>
|
||||
<div class="promoter-detail__title">{{ detail.nick_name || '-' }}</div>
|
||||
<div class="promoter-detail__role">{{ detail.role_name || '业务员' }}</div>
|
||||
</div>
|
||||
<Descriptions bordered :column="1" size="small" class="promoter-detail__desc">
|
||||
<Descriptions.Item label="推广码">{{ detail.code || '-' }}</Descriptions.Item>
|
||||
<Descriptions.Item label="手机号">{{ detail.phone || '-' }}</Descriptions.Item>
|
||||
<Descriptions.Item label="录入诊所">
|
||||
{{ detail.clinic_input_count ?? 0 }} 家
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="录入药店">
|
||||
{{ detail.pharmacy_input_count ?? 0 }} 家
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
</Spin>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.promoter-detail__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.promoter-detail__title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
.promoter-detail__role {
|
||||
font-size: 13px;
|
||||
color: #86909c;
|
||||
}
|
||||
|
||||
.promoter-detail__desc {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.dark .promoter-detail__title {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.dark .promoter-detail__role {
|
||||
color: #9ca3af;
|
||||
}
|
||||
</style>
|
||||
237
apps/web-antd/src/components/promoter/PromoterInfoCard.vue
Normal file
237
apps/web-antd/src/components/promoter/PromoterInfoCard.vue
Normal file
@@ -0,0 +1,237 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Avatar } from 'ant-design-vue';
|
||||
|
||||
import { resolveAvatarUrl } from '#/views/system/store-input/utils/inputUser';
|
||||
|
||||
import PromoterDetailModal from './PromoterDetailModal.vue';
|
||||
|
||||
export type PromoterAdminLike = {
|
||||
id?: number;
|
||||
nick_name?: string;
|
||||
avatar?: string;
|
||||
phone?: string;
|
||||
code?: string;
|
||||
role_name?: string;
|
||||
};
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
admin?: PromoterAdminLike | null;
|
||||
size?: 'compact' | 'trigger';
|
||||
clickable?: boolean;
|
||||
showClear?: boolean;
|
||||
disabled?: boolean;
|
||||
}>(),
|
||||
{
|
||||
admin: null,
|
||||
size: 'compact',
|
||||
clickable: true,
|
||||
showClear: false,
|
||||
disabled: false,
|
||||
},
|
||||
);
|
||||
|
||||
const emits = defineEmits<{
|
||||
clear: [];
|
||||
}>();
|
||||
|
||||
const [DetailModal, detailModalApi] = useVbenModal({
|
||||
connectedComponent: PromoterDetailModal,
|
||||
});
|
||||
|
||||
const displayName = computed(
|
||||
() => props.admin?.nick_name?.trim() || props.admin?.phone?.trim() || '-',
|
||||
);
|
||||
|
||||
const avatarUrl = computed(() => resolveAvatarUrl(props.admin?.avatar));
|
||||
|
||||
const canOpenDetail = computed(
|
||||
() =>
|
||||
props.clickable &&
|
||||
!props.disabled &&
|
||||
!!(props.admin?.id || props.admin?.code),
|
||||
);
|
||||
|
||||
function openDetail() {
|
||||
if (!canOpenDetail.value) return;
|
||||
detailModalApi.setData({
|
||||
id: props.admin?.id,
|
||||
code: props.admin?.code,
|
||||
});
|
||||
detailModalApi.open();
|
||||
}
|
||||
|
||||
function onClear(e: Event) {
|
||||
e.stopPropagation();
|
||||
emits('clear');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<DetailModal />
|
||||
<div
|
||||
v-if="admin"
|
||||
class="promoter-info-card"
|
||||
:class="[
|
||||
`promoter-info-card--${size}`,
|
||||
{
|
||||
'promoter-info-card--clickable': canOpenDetail,
|
||||
'promoter-info-card--disabled': disabled,
|
||||
},
|
||||
]"
|
||||
@click="openDetail"
|
||||
>
|
||||
<Avatar v-if="avatarUrl" :src="avatarUrl" :size="size === 'trigger' ? 36 : 28" />
|
||||
<Avatar v-else :size="size === 'trigger' ? 36 : 28">
|
||||
{{ displayName.charAt(0) }}
|
||||
</Avatar>
|
||||
<div class="promoter-info-card__meta">
|
||||
<div class="promoter-info-card__name">{{ displayName }}</div>
|
||||
<div v-if="size === 'trigger' && admin.code" class="promoter-info-card__sub">
|
||||
业务码:{{ admin.code }}
|
||||
</div>
|
||||
<div v-else-if="admin.phone" class="promoter-info-card__sub">
|
||||
{{ admin.phone }}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
v-if="showClear && !disabled"
|
||||
class="promoter-info-card__clear"
|
||||
@click="onClear"
|
||||
>
|
||||
×
|
||||
</span>
|
||||
</div>
|
||||
<span v-else class="promoter-info-card__empty">-</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.promoter-info-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.promoter-info-card--compact {
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.promoter-info-card--trigger {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e5e6eb;
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
background-color 0.2s;
|
||||
}
|
||||
|
||||
.promoter-info-card--clickable.promoter-info-card--trigger {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.promoter-info-card--clickable.promoter-info-card--trigger:hover {
|
||||
border-color: #165dff;
|
||||
background: #f2f7ff;
|
||||
}
|
||||
|
||||
.promoter-info-card--clickable.promoter-info-card--compact {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.promoter-info-card--clickable.promoter-info-card--compact:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.promoter-info-card--disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.promoter-info-card__meta {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.promoter-info-card__name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1d2129;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.promoter-info-card--compact .promoter-info-card__name {
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.promoter-info-card__sub {
|
||||
font-size: 12px;
|
||||
color: #86909c;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.promoter-info-card--compact .promoter-info-card__sub {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.promoter-info-card__clear {
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
color: #86909c;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.promoter-info-card__clear:hover {
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
.promoter-info-card__empty {
|
||||
color: #86909c;
|
||||
}
|
||||
|
||||
.dark {
|
||||
.promoter-info-card--trigger {
|
||||
background: #1f2937;
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
.promoter-info-card--clickable.promoter-info-card--trigger:hover {
|
||||
background: #374151;
|
||||
border-color: #60a5fa;
|
||||
}
|
||||
|
||||
.promoter-info-card__name {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.promoter-info-card__sub,
|
||||
.promoter-info-card__empty {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.promoter-info-card__clear {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.promoter-info-card__clear:hover {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
71
apps/web-antd/src/composables/useOrderPriceAdjust.ts
Normal file
71
apps/web-antd/src/composables/useOrderPriceAdjust.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { getPriceAdjustConfig } from '#/views/system/system-config/api';
|
||||
|
||||
import type { QuickDiscountOption } from '#/utils/pricePercentAdjust';
|
||||
import { applyRatioToDrug, applyRatioToDrugs, formatPriceDiscountLabel, normalizeQuickOptions } from '#/utils/pricePercentAdjust';
|
||||
|
||||
export type PriceAdjustConfig = {
|
||||
order_price_adjust_scope: 'both' | 'sale_only';
|
||||
order_discount_quick_options: QuickDiscountOption[];
|
||||
enable_order_price_percent_adjust: boolean;
|
||||
store_id?: number;
|
||||
};
|
||||
|
||||
const defaultConfig: PriceAdjustConfig = {
|
||||
order_price_adjust_scope: 'sale_only',
|
||||
order_discount_quick_options: normalizeQuickOptions([]),
|
||||
enable_order_price_percent_adjust: false,
|
||||
};
|
||||
|
||||
export function useOrderPriceAdjust() {
|
||||
const config = ref<PriceAdjustConfig>({ ...defaultConfig });
|
||||
const loading = ref(false);
|
||||
|
||||
async function loadByStoreId(storeId?: number) {
|
||||
if (!storeId) {
|
||||
config.value = { ...defaultConfig };
|
||||
return config.value;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getPriceAdjustConfig(storeId);
|
||||
config.value = {
|
||||
order_price_adjust_scope: res?.order_price_adjust_scope === 'both' ? 'both' : 'sale_only',
|
||||
order_discount_quick_options: normalizeQuickOptions(res?.order_discount_quick_options),
|
||||
enable_order_price_percent_adjust: !!res?.enable_order_price_percent_adjust,
|
||||
store_id: storeId,
|
||||
};
|
||||
return config.value;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function setFromOrderStore(store?: { enable_order_price_percent_adjust?: number | boolean }) {
|
||||
config.value = {
|
||||
...config.value,
|
||||
enable_order_price_percent_adjust: Number(store?.enable_order_price_percent_adjust) === 1,
|
||||
};
|
||||
}
|
||||
|
||||
async function loadGlobalOptions() {
|
||||
const res = await getPriceAdjustConfig();
|
||||
config.value = {
|
||||
...config.value,
|
||||
order_price_adjust_scope: res?.order_price_adjust_scope === 'both' ? 'both' : 'sale_only',
|
||||
order_discount_quick_options: normalizeQuickOptions(res?.order_discount_quick_options),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
config,
|
||||
loading,
|
||||
loadByStoreId,
|
||||
loadGlobalOptions,
|
||||
setFromOrderStore,
|
||||
applyRatioToDrug,
|
||||
applyRatioToDrugs,
|
||||
formatPriceDiscountLabel,
|
||||
};
|
||||
}
|
||||
108
apps/web-antd/src/utils/pricePercentAdjust.ts
Normal file
108
apps/web-antd/src/utils/pricePercentAdjust.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
export type QuickDiscountOption = { name: string; value: number };
|
||||
|
||||
export const DEFAULT_QUICK_OPTIONS: QuickDiscountOption[] = [
|
||||
{ name: '九五折', value: 95 },
|
||||
{ name: '九折', value: 90 },
|
||||
{ name: '八五折', value: 85 },
|
||||
{ name: '八折', value: 80 },
|
||||
{ name: '涨10%', value: 110 },
|
||||
{ name: '涨20%', value: 120 },
|
||||
];
|
||||
|
||||
const DEFAULT_MARKUP_OPTIONS: QuickDiscountOption[] = [
|
||||
{ name: '涨10%', value: 110 },
|
||||
{ name: '涨20%', value: 120 },
|
||||
];
|
||||
|
||||
export function normalizeQuickOptions(raw: unknown): QuickDiscountOption[] {
|
||||
if (!Array.isArray(raw)) return [...DEFAULT_QUICK_OPTIONS];
|
||||
const list: QuickDiscountOption[] = [];
|
||||
for (const item of raw) {
|
||||
if (item && typeof item === 'object' && 'value' in item) {
|
||||
const val = Number((item as QuickDiscountOption).value);
|
||||
if (val > 0) {
|
||||
list.push({
|
||||
name: String((item as QuickDiscountOption).name || `${val}%`),
|
||||
value: val,
|
||||
});
|
||||
}
|
||||
} else if (typeof item === 'number' && item > 0) {
|
||||
list.push({ name: `${item}%`, value: item });
|
||||
}
|
||||
}
|
||||
if (!list.length) return [...DEFAULT_QUICK_OPTIONS];
|
||||
if (!list.some((o) => o.value > 100)) {
|
||||
return [...list, ...DEFAULT_MARKUP_OPTIONS];
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
export function applyRatioPrice(originPrice: number, discount: number): number {
|
||||
if (!originPrice || originPrice <= 0) return 0;
|
||||
const ratio = discount > 0 ? discount : 100;
|
||||
return Math.round(originPrice * (ratio / 100) * 10000) / 10000;
|
||||
}
|
||||
|
||||
export function inferDiscountKind(discount: number): 'none' | 'discount' | 'markup' {
|
||||
const d = Number(discount) || 100;
|
||||
if (d === 100) return 'none';
|
||||
return d < 100 ? 'discount' : 'markup';
|
||||
}
|
||||
|
||||
export function formatPriceDiscountLabel(discount: number): string | null {
|
||||
const d = Number(discount) || 100;
|
||||
if (d <= 0 || d === 100) return null;
|
||||
if (d < 100) {
|
||||
if (d % 10 === 0 && d >= 10) return `${d / 10}折`;
|
||||
return `折扣 ${d}%`;
|
||||
}
|
||||
return `涨 ${d - 100}%`;
|
||||
}
|
||||
|
||||
export function resolvePriceDiscount(data?: {
|
||||
priceDiscount?: number | string;
|
||||
price_discount?: number | string;
|
||||
}): number {
|
||||
const raw = data?.priceDiscount ?? data?.price_discount;
|
||||
const n = Number(raw);
|
||||
return n > 0 ? n : 100;
|
||||
}
|
||||
|
||||
export function applyRatioToDrug<T extends { price?: number | string; buy_price?: number | string; origin_price?: number | string; origin_buy_price?: number | string }>(
|
||||
drug: T,
|
||||
discount: number,
|
||||
scope: 'both' | 'sale_only',
|
||||
): T {
|
||||
const originPrice = Number(drug.origin_price ?? drug.price ?? 0);
|
||||
const originBuy = Number(drug.origin_buy_price ?? drug.buy_price ?? 0);
|
||||
const next: T = {
|
||||
...drug,
|
||||
origin_price: drug.origin_price ?? originPrice,
|
||||
origin_buy_price: drug.origin_buy_price ?? originBuy,
|
||||
price: applyRatioPrice(originPrice, discount),
|
||||
};
|
||||
if (scope === 'both' && originBuy > 0) {
|
||||
next.buy_price = applyRatioPrice(originBuy, discount);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
export function applyRatioToDrugs<T extends { price?: number | string; buy_price?: number | string; origin_price?: number | string; origin_buy_price?: number | string }>(
|
||||
drugs: T[],
|
||||
discount: number,
|
||||
scope: 'both' | 'sale_only',
|
||||
): T[] {
|
||||
return drugs.map((d) => applyRatioToDrug(d, discount, scope));
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
export type AdjustType = 'markup' | 'discount';
|
||||
|
||||
/** @deprecated use applyRatioPrice */
|
||||
export function applyPercentPrice(oldPrice: number, adjustType: AdjustType, percent: number): number {
|
||||
if (!oldPrice || oldPrice <= 0) return 0;
|
||||
if (adjustType === 'markup') {
|
||||
return applyRatioPrice(oldPrice, Math.round(100 + percent));
|
||||
}
|
||||
return applyRatioPrice(oldPrice, percent);
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
|
||||
import { Button, Form, InputNumber, message, Space, Descriptions, Tag } from 'ant-design-vue';
|
||||
|
||||
import type { QuickDiscountOption } from '#/utils/pricePercentAdjust';
|
||||
import {
|
||||
applyRatioPrice,
|
||||
formatPriceDiscountLabel,
|
||||
normalizeQuickOptions,
|
||||
resolvePriceDiscount,
|
||||
} from '#/utils/pricePercentAdjust';
|
||||
import { adjustOrderPercent } from '#/api/order/priceAdjust';
|
||||
|
||||
defineOptions({ name: 'OrderPricePercentAdjustDrawer' });
|
||||
|
||||
type DrawerData = {
|
||||
stage: 'pre_send' | 'post_order';
|
||||
priceDiscount?: number;
|
||||
price_discount?: number;
|
||||
productOrderId?: number;
|
||||
registerId?: number;
|
||||
priceAdjustScope?: 'both' | 'sale_only';
|
||||
quickOptions?: QuickDiscountOption[];
|
||||
previewOriginPrice?: number;
|
||||
onApplyPreSend?: (payload: { priceDiscount: number }) => void;
|
||||
onSuccess?: () => void;
|
||||
};
|
||||
|
||||
const loading = ref(false);
|
||||
const priceDiscount = ref<number>(100);
|
||||
const formData = ref<DrawerData>({ stage: 'pre_send', quickOptions: [] });
|
||||
|
||||
const currentLabel = computed(() => formatPriceDiscountLabel(priceDiscount.value));
|
||||
|
||||
const previewPrice = computed(() => {
|
||||
const base = Number(formData.value.previewOriginPrice ?? 0);
|
||||
if (!base) return null;
|
||||
return applyRatioPrice(base, priceDiscount.value);
|
||||
});
|
||||
|
||||
const discountQuickOptions = computed(() =>
|
||||
(formData.value.quickOptions || []).filter((o) => o.value < 100),
|
||||
);
|
||||
const markupQuickOptions = computed(() =>
|
||||
(formData.value.quickOptions || []).filter((o) => o.value > 100),
|
||||
);
|
||||
|
||||
const [DrawerComponent, drawerApi] = useVbenDrawer({
|
||||
class: 'w-[520px]',
|
||||
onConfirm: async () => {
|
||||
const d = Number(priceDiscount.value);
|
||||
if (!d || d <= 0) {
|
||||
message.warning('请输入有效比例值');
|
||||
return;
|
||||
}
|
||||
if (formData.value.stage === 'pre_send') {
|
||||
formData.value.onApplyPreSend?.({ priceDiscount: d });
|
||||
drawerApi.close();
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
await adjustOrderPercent({
|
||||
product_order_id: formData.value.productOrderId,
|
||||
price_discount: d,
|
||||
register_id: formData.value.registerId || 0,
|
||||
});
|
||||
message.success(d === 100 ? '已清除价格浮动' : '调价成功');
|
||||
formData.value.onSuccess?.();
|
||||
drawerApi.close();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '调价失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = { stage: 'pre_send', quickOptions: [] };
|
||||
priceDiscount.value = 100;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function syncFromDrawerData() {
|
||||
const data = drawerApi.getData<DrawerData>();
|
||||
const discount = resolvePriceDiscount(data);
|
||||
formData.value = {
|
||||
...data,
|
||||
stage: data.stage || 'pre_send',
|
||||
quickOptions: normalizeQuickOptions(data.quickOptions),
|
||||
priceDiscount: discount,
|
||||
price_discount: discount,
|
||||
};
|
||||
priceDiscount.value = discount;
|
||||
}
|
||||
|
||||
function scheduleSyncFromDrawerData() {
|
||||
void nextTick(() => {
|
||||
syncFromDrawerData();
|
||||
void nextTick(() => syncFromDrawerData());
|
||||
});
|
||||
}
|
||||
|
||||
const drawerIsOpen = drawerApi.useStore((state) => state.isOpen);
|
||||
|
||||
watch(drawerIsOpen, (isOpen) => {
|
||||
if (isOpen) {
|
||||
scheduleSyncFromDrawerData();
|
||||
}
|
||||
});
|
||||
|
||||
function pickQuick(opt: QuickDiscountOption) {
|
||||
priceDiscount.value = opt.value;
|
||||
}
|
||||
|
||||
function clearFloat() {
|
||||
priceDiscount.value = 100;
|
||||
}
|
||||
|
||||
defineExpose({ drawerApi });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DrawerComponent title="整单调价" :loading="loading">
|
||||
<Descriptions v-if="currentLabel" :column="1" bordered size="small" class="mb-4">
|
||||
<Descriptions.Item label="当前浮动">
|
||||
<Tag color="processing">{{ currentLabel }}({{ priceDiscount }}%)</Tag>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="价格比例(100=原价,90=九折,120=涨20%)">
|
||||
<InputNumber v-model:value="priceDiscount" :min="1" :max="500" class="w-full" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item v-if="discountQuickOptions.length" label="快捷打折">
|
||||
<Space wrap>
|
||||
<Button
|
||||
v-for="opt in discountQuickOptions"
|
||||
:key="'d-' + opt.value"
|
||||
size="small"
|
||||
@click="pickQuick(opt)"
|
||||
>
|
||||
{{ opt.name }}({{ opt.value }}%)
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item v-if="markupQuickOptions.length" label="快捷涨价">
|
||||
<Space wrap>
|
||||
<Button
|
||||
v-for="opt in markupQuickOptions"
|
||||
:key="'m-' + opt.value"
|
||||
size="small"
|
||||
@click="pickQuick(opt)"
|
||||
>
|
||||
{{ opt.name }}({{ opt.value }}%)
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item v-if="previewPrice != null" label="示例:原价100预览">
|
||||
<span class="text-primary font-medium">¥{{ previewPrice.toFixed(4) }}</span>
|
||||
</Form.Item>
|
||||
|
||||
<Button block @click="clearFloat">清除浮动(恢复100%)</Button>
|
||||
</Form>
|
||||
</DrawerComponent>
|
||||
</template>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
@@ -7,6 +7,11 @@ import { Button, Card, Descriptions, Image, Space, Tag, Timeline } from 'ant-des
|
||||
|
||||
import OrderTraceDrawer from '#/views/business/order/components/order-trace-drawer.vue';
|
||||
|
||||
import OrderPricePercentAdjustDrawer from '#/views/business/order/components/OrderPricePercentAdjustDrawer.vue';
|
||||
import { getOrderPriceAdjustConfig, adjustOrderPercent } from '#/api/order/priceAdjust';
|
||||
import type { QuickDiscountOption } from '#/utils/pricePercentAdjust';
|
||||
import { formatPriceDiscountLabel, normalizeQuickOptions } from '#/utils/pricePercentAdjust';
|
||||
|
||||
import { expressDetailByOrderId, getOrderInfo } from '../api';
|
||||
|
||||
defineOptions({
|
||||
@@ -25,6 +30,64 @@ const [TraceDrawer, traceDrawerApi] = useVbenDrawer({
|
||||
connectedComponent: OrderTraceDrawer,
|
||||
});
|
||||
|
||||
const [PercentAdjustDrawer, percentAdjustDrawerApi] = useVbenDrawer({
|
||||
connectedComponent: OrderPricePercentAdjustDrawer,
|
||||
});
|
||||
|
||||
const priceAdjustMeta = ref({
|
||||
scope: 'sale_only' as 'both' | 'sale_only',
|
||||
quickOptions: [] as QuickDiscountOption[],
|
||||
});
|
||||
|
||||
const priceDiscountLabel = computed(() =>
|
||||
formatPriceDiscountLabel(Number(data.value?.price_discount ?? 100)),
|
||||
);
|
||||
|
||||
const canPercentAdjust = computed(() => {
|
||||
return (
|
||||
Number(data.value?.is_pay) !== 1 &&
|
||||
Number(data.value?.store?.enable_order_price_percent_adjust) === 1
|
||||
);
|
||||
});
|
||||
|
||||
async function reloadOrder() {
|
||||
if (!data.value?.id) return;
|
||||
data.value = await getOrderInfo(data.value.id);
|
||||
}
|
||||
|
||||
async function openOrderPercentAdjust() {
|
||||
await reloadOrder();
|
||||
const discount = Number(data.value?.price_discount ?? 100);
|
||||
percentAdjustDrawerApi.setData({
|
||||
stage: 'post_order',
|
||||
priceDiscount: discount,
|
||||
price_discount: discount,
|
||||
productOrderId: data.value.id,
|
||||
quickOptions: normalizeQuickOptions(priceAdjustMeta.value.quickOptions),
|
||||
onSuccess: reloadOrder,
|
||||
});
|
||||
percentAdjustDrawerApi.open();
|
||||
}
|
||||
|
||||
async function clearOrderPriceDiscount() {
|
||||
if (!data.value?.id) return;
|
||||
await adjustOrderPercent({
|
||||
product_order_id: data.value.id,
|
||||
price_discount: 100,
|
||||
});
|
||||
await reloadOrder();
|
||||
}
|
||||
|
||||
async function loadPriceAdjustMeta() {
|
||||
const cfg = await getOrderPriceAdjustConfig();
|
||||
priceAdjustMeta.value = {
|
||||
scope: cfg?.order_price_adjust_scope === 'both' ? 'both' : 'sale_only',
|
||||
quickOptions: normalizeQuickOptions(cfg?.order_discount_quick_options),
|
||||
};
|
||||
}
|
||||
|
||||
loadPriceAdjustMeta();
|
||||
|
||||
function openOrderTrace() {
|
||||
if (!data.value?.id) {
|
||||
return;
|
||||
@@ -100,6 +163,34 @@ const orderTypeMap = {
|
||||
1: '处方订单',
|
||||
2: '预约购药订单',
|
||||
3: '商城处方订单',
|
||||
};
|
||||
|
||||
const prescriptionStatusMap: Record<number, string> = {
|
||||
0: '待审核',
|
||||
1: '已审核',
|
||||
2: '已驳回',
|
||||
3: '已过期',
|
||||
};
|
||||
|
||||
const showPrescriptionStatus = computed(() => {
|
||||
const row = data.value;
|
||||
if (!row?.p_id || row.order_type === 2 || row.order_type === 3) {
|
||||
return false;
|
||||
}
|
||||
return row.prescription?.status != null;
|
||||
});
|
||||
|
||||
function prescriptionStatusText() {
|
||||
const status = data.value?.prescription?.status;
|
||||
return prescriptionStatusMap[Number(status)] ?? '未知';
|
||||
}
|
||||
|
||||
function prescriptionStatusColor() {
|
||||
const status = Number(data.value?.prescription?.status);
|
||||
if (status === 1) return 'green';
|
||||
if (status === 2) return 'red';
|
||||
if (status === 0) return 'orange';
|
||||
return 'default';
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
@@ -126,7 +217,28 @@ const orderTypeMap = {
|
||||
{{ data.total_pay_price }} 元
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="物品总价">
|
||||
药品:{{ data.items_price }} 元, 加工费:{{ data.process_price }} 元
|
||||
药品:
|
||||
<Button
|
||||
v-if="canPercentAdjust"
|
||||
type="link"
|
||||
class="!p-0"
|
||||
@click="openOrderPercentAdjust"
|
||||
>
|
||||
{{ data.items_price }} 元
|
||||
</Button>
|
||||
<template v-else>{{ data.items_price }} 元</template>
|
||||
, 加工费:{{ data.process_price }} 元
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item v-if="priceDiscountLabel" label="价格浮动">
|
||||
{{ priceDiscountLabel }}({{ data.price_discount }}%)
|
||||
<Button
|
||||
v-if="canPercentAdjust && Number(data.price_discount) !== 100"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="clearOrderPriceDiscount"
|
||||
>
|
||||
清除浮动
|
||||
</Button>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="供货价">
|
||||
{{ data.market_price }} 元
|
||||
@@ -173,6 +285,11 @@ const orderTypeMap = {
|
||||
<Tag v-else-if="data.prescription_type === 7" color="geekblue">医疗器械</Tag>
|
||||
<Tag v-else>其他</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item v-if="showPrescriptionStatus" label="处方状态">
|
||||
<Tag :color="prescriptionStatusColor()">
|
||||
{{ prescriptionStatusText() }}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<div class="mt-4">
|
||||
@@ -343,6 +460,7 @@ const orderTypeMap = {
|
||||
</div>
|
||||
</Modal>
|
||||
<TraceDrawer />
|
||||
<PercentAdjustDrawer />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { VbenFormProps } from '#/adapter/form';
|
||||
|
||||
import { getExpressCompaniesOption } from '#/views/business/express/express-company/api';
|
||||
|
||||
export const modalFormProps: VbenFormProps = {
|
||||
wrapperClass: 'grid-cols-12', // 24栅格,
|
||||
commonConfig: {
|
||||
@@ -34,24 +32,9 @@ export const modalFormProps: VbenFormProps = {
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
component: 'ExpressCompanySelect',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
// filterOption: true,
|
||||
showSearch: true,
|
||||
filterOption: (input: string, option: any) => {
|
||||
// 自定义过滤逻辑,确保可以根据 name 进行搜索
|
||||
return option?.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
},
|
||||
// 菜单接口转options格式
|
||||
afterFetch: (data: { id: number; name: string }[]) => {
|
||||
return data.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.code,
|
||||
}));
|
||||
},
|
||||
api: getExpressCompaniesOption,
|
||||
placeholder: '请选择',
|
||||
placeholder: '请选择快递公司',
|
||||
},
|
||||
fieldName: 'express_company_code',
|
||||
formItemClass: 'col-span-6',
|
||||
|
||||
@@ -67,19 +67,19 @@ export const gridOptions: VxeGridProps<RowType> = {
|
||||
width: 100,
|
||||
slots: { default: 'is-free-shipping' },
|
||||
},
|
||||
{
|
||||
field: 'is_online',
|
||||
title: '订单来源',
|
||||
width: 100,
|
||||
slots: { default: 'is-online' },
|
||||
},
|
||||
{
|
||||
field: 'pay_time',
|
||||
title: '支付时间',
|
||||
width: 240,
|
||||
slots: { default: 'pay-time' },
|
||||
},
|
||||
{ field: 'created_at', width: 240, title: '下单时间' },
|
||||
// {
|
||||
// field: 'is_online',
|
||||
// title: '订单来源',
|
||||
// width: 100,
|
||||
// slots: { default: 'is-online' },
|
||||
// },
|
||||
// {
|
||||
// field: 'pay_time',
|
||||
// title: '支付时间',
|
||||
// width: 240,
|
||||
// slots: { default: 'pay-time' },
|
||||
// },
|
||||
// { field: 'created_at', width: 240, title: '下单时间' },
|
||||
{
|
||||
type: 'html',
|
||||
title: '操作',
|
||||
|
||||
@@ -20,6 +20,7 @@ import { TableAction } from '#/components/table-action';
|
||||
import { downloadByData } from '#/util/tool';
|
||||
import {
|
||||
exportOrderApi,
|
||||
getOrderInfo,
|
||||
getOrderList,
|
||||
getVerifyRecentOrderAmounts,
|
||||
saleAmountApi,
|
||||
@@ -28,6 +29,10 @@ import {
|
||||
import { simulatePayApi, accrueSalespersonCommissionApi } from '#/views/business/order/api/order-ops';
|
||||
import ChinaErpSyncLogDrawer from '#/views/business/order/components/china-erp-sync-log-drawer.vue';
|
||||
import OrderTraceDrawer from '#/views/business/order/components/order-trace-drawer.vue';
|
||||
import OrderPricePercentAdjustDrawer from '#/views/business/order/components/OrderPricePercentAdjustDrawer.vue';
|
||||
import { getOrderPriceAdjustConfig } from '#/api/order/priceAdjust';
|
||||
import type { QuickDiscountOption } from '#/utils/pricePercentAdjust';
|
||||
import { normalizeQuickOptions } from '#/utils/pricePercentAdjust';
|
||||
import PrescriptionDetail from '#/views/doctor/doctor-reception/components/PrescriptionDetail.vue';
|
||||
|
||||
import DetailModal from './components/detail.vue';
|
||||
@@ -192,6 +197,52 @@ const [ChinaErpSyncDrawer, chinaErpSyncDrawerApi] = useVbenDrawer({
|
||||
connectedComponent: ChinaErpSyncLogDrawer,
|
||||
});
|
||||
|
||||
const [PercentAdjustDrawer, percentAdjustDrawerApi] = useVbenDrawer({
|
||||
connectedComponent: OrderPricePercentAdjustDrawer,
|
||||
});
|
||||
|
||||
const priceAdjustMeta = ref({
|
||||
quickOptions: [] as QuickDiscountOption[],
|
||||
});
|
||||
|
||||
async function loadPriceAdjustMeta() {
|
||||
const cfg = await getOrderPriceAdjustConfig();
|
||||
priceAdjustMeta.value = {
|
||||
quickOptions: normalizeQuickOptions(cfg?.order_discount_quick_options),
|
||||
};
|
||||
}
|
||||
|
||||
void loadPriceAdjustMeta();
|
||||
|
||||
function canRowPercentAdjust(row: Record<string, any>) {
|
||||
return (
|
||||
Number(row?.is_pay) !== 1 &&
|
||||
Number(row?.store?.enable_order_price_percent_adjust) === 1
|
||||
);
|
||||
}
|
||||
|
||||
async function openOrderPercentAdjust(row: Record<string, any>) {
|
||||
if (!canRowPercentAdjust(row)) return;
|
||||
let discount = Number(row.price_discount ?? 100);
|
||||
try {
|
||||
const detail = await getOrderInfo(row.id);
|
||||
discount = Number(detail?.price_discount ?? discount);
|
||||
} catch {
|
||||
// 列表字段缺失时沿用行内值
|
||||
}
|
||||
percentAdjustDrawerApi.setData({
|
||||
stage: 'post_order',
|
||||
priceDiscount: discount,
|
||||
price_discount: discount,
|
||||
productOrderId: row.id,
|
||||
quickOptions: normalizeQuickOptions(priceAdjustMeta.value.quickOptions),
|
||||
onSuccess: () => {
|
||||
gridApi.reload();
|
||||
},
|
||||
});
|
||||
percentAdjustDrawerApi.open();
|
||||
}
|
||||
|
||||
function openChinaErpSyncLog(row: Record<string, any>) {
|
||||
chinaErpSyncDrawerApi.setData({
|
||||
order_id: row.id,
|
||||
@@ -454,11 +505,19 @@ const openOrderAmountVerify = () => {
|
||||
<template #avatar="{ row }">
|
||||
<Image :src="row.user.avatarurl || '/img/user-default-avatar.png'" />
|
||||
{{ row.user.nickname }}
|
||||
<div>ID:{{ row.user.id }}</div>
|
||||
</template>
|
||||
<template #order-store="{ row }">
|
||||
<div class="leading-snug">
|
||||
<div class="mb-1">
|
||||
<Tag v-if="row.is_online === 1" color="blue">在线问诊</Tag>
|
||||
<Tag v-else-if="row.is_online === 2" color="green">在线复诊(诊所)</Tag>
|
||||
<Tag v-else-if="row.is_online === 3" color="green">在线复诊(药店)</Tag>
|
||||
<Tag v-else color="default">线下就诊</Tag>
|
||||
</div>
|
||||
<div class="font-medium">{{ row.order_no }}</div>
|
||||
<div class="text-xs text-gray-500">{{ row.store?.name || '—' }}</div>
|
||||
<div class="text-xs text-gray-600">下单时间: {{ row.created_at || '—' }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #express-info="{ row }">
|
||||
@@ -480,8 +539,30 @@ const openOrderAmountVerify = () => {
|
||||
</template>
|
||||
<template #price-info="{ row }">
|
||||
<div class="space-y-0.5 text-sm leading-snug">
|
||||
<div>药品:¥{{ row.items_price ?? '0.00' }}</div>
|
||||
<div>支付:¥{{ row.total_pay_price ?? '0.00' }}</div>
|
||||
<div>
|
||||
药品:
|
||||
<Button
|
||||
v-if="canRowPercentAdjust(row)"
|
||||
class="!h-auto !px-0 !py-0"
|
||||
type="link"
|
||||
@click="openOrderPercentAdjust(row)"
|
||||
>
|
||||
¥{{ row.items_price ?? '0.00' }}
|
||||
</Button>
|
||||
<template v-else>¥{{ row.items_price ?? '0.00' }}</template>
|
||||
</div>
|
||||
<div>
|
||||
支付:
|
||||
<Button
|
||||
v-if="canRowPercentAdjust(row)"
|
||||
class="!h-auto !px-0 !py-0"
|
||||
type="link"
|
||||
@click="openOrderPercentAdjust(row)"
|
||||
>
|
||||
¥{{ row.total_pay_price ?? '0.00' }}
|
||||
</Button>
|
||||
<template v-else>¥{{ row.total_pay_price ?? '0.00' }}</template>
|
||||
</div>
|
||||
<div>
|
||||
挂号:
|
||||
<Button
|
||||
@@ -811,6 +892,7 @@ const openOrderAmountVerify = () => {
|
||||
</div>
|
||||
</template>
|
||||
</Grid>
|
||||
<PercentAdjustDrawer />
|
||||
</Page>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -23,13 +23,13 @@ export const gridOptions: VxeGridProps<RegisterOrderItem> = {
|
||||
{ field: 'is_pay', title: '是否支付', width: 100, slots: { default: 'is_pay' } },
|
||||
{ field: 'status', title: '状态', slots: { default: 'status' } },
|
||||
{ field: 'created_at', title: '创建时间' },
|
||||
{
|
||||
type: 'html',
|
||||
title: '操作',
|
||||
align: 'right',
|
||||
slots: { default: 'action' },
|
||||
width: 200,
|
||||
},
|
||||
// {
|
||||
// type: 'html',
|
||||
// title: '操作',
|
||||
// align: 'right',
|
||||
// slots: { default: 'action' },
|
||||
// width: 200,
|
||||
// },
|
||||
],
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, ref, watch} from 'vue';
|
||||
|
||||
import {Page, useVbenModal} from '@vben/common-ui';
|
||||
import {Page, useVbenDrawer, useVbenModal} from '@vben/common-ui';
|
||||
import {useUserStore} from '@vben/stores';
|
||||
|
||||
import {
|
||||
@@ -81,6 +81,13 @@ import {
|
||||
calcTotalMarginPercent,
|
||||
isSeeRateEnabled,
|
||||
} from '#/utils/chinesePrescriptionMargin';
|
||||
import OrderPricePercentAdjustDrawer from '#/views/business/order/components/OrderPricePercentAdjustDrawer.vue';
|
||||
import { useOrderPriceAdjust } from '#/composables/useOrderPriceAdjust';
|
||||
import {
|
||||
applyRatioToDrugs,
|
||||
formatPriceDiscountLabel,
|
||||
normalizeQuickOptions,
|
||||
} from '#/utils/pricePercentAdjust';
|
||||
|
||||
interface Patient {
|
||||
id: number;
|
||||
@@ -139,6 +146,14 @@ const myStoreId = ref(userStore.userInfo.store_id);
|
||||
const seeRate = ref(0);
|
||||
/** 开方是否可选医保(0=仅自费) */
|
||||
const allowInsuranceCategory = ref(0);
|
||||
/** 是否开启订单百分比调价 */
|
||||
const priceAdjustEnabled = ref(false);
|
||||
/** 开方购物车整单价格比例,100=原价 */
|
||||
const priceDiscount = ref(100);
|
||||
const { config: priceAdjustConfig, loadByStoreId, applyRatioToDrugs: applyRatioDrugs } = useOrderPriceAdjust();
|
||||
const [PriceAdjustDrawer, priceAdjustDrawerApi] = useVbenDrawer({
|
||||
connectedComponent: OrderPricePercentAdjustDrawer,
|
||||
});
|
||||
/** 推广员传方导入关联ID */
|
||||
const salespersonTransferPrescriptionId = ref(0);
|
||||
const salespersonTransferDrawerOpen = ref(false);
|
||||
@@ -170,6 +185,8 @@ async function fetchStoreSeeRate() {
|
||||
const res = await getCurrentStoreTypeApi(params);
|
||||
seeRate.value = Number(res?.see_rate ?? 0);
|
||||
allowInsuranceCategory.value = Number(res?.allow_insurance_category ?? 0);
|
||||
priceAdjustEnabled.value = Number(res?.enable_order_price_percent_adjust ?? 0) === 1;
|
||||
await loadByStoreId(myStoreId.value);
|
||||
if (allowInsuranceCategory.value !== 1) {
|
||||
category.value = 1;
|
||||
}
|
||||
@@ -281,7 +298,7 @@ function updatePatientList() {
|
||||
|
||||
getPatientListByReception();
|
||||
// 每5秒更新数据
|
||||
setInterval(getPatientListByReception, 600_000);
|
||||
setInterval(getPatientListByReception, 30_000);
|
||||
|
||||
// 药品分类
|
||||
const categories = [
|
||||
@@ -566,6 +583,44 @@ const saveToLocalStorage = () => {
|
||||
JSON.stringify(currentDrugs.value),
|
||||
);
|
||||
};
|
||||
|
||||
function snapshotDrugOrigins(drugs: any[]) {
|
||||
return drugs.map((drug) => ({
|
||||
...drug,
|
||||
origin_price: drug.origin_price ?? drug.price,
|
||||
origin_buy_price: drug.origin_buy_price ?? drug.buy_price,
|
||||
}));
|
||||
}
|
||||
|
||||
function applyCartPriceDiscount(discount: number) {
|
||||
priceDiscount.value = discount;
|
||||
const scope = priceAdjustConfig.value.order_price_adjust_scope;
|
||||
currentDrugs.value = applyRatioDrugs(snapshotDrugOrigins(currentDrugs.value), discount, scope);
|
||||
saveToLocalStorage();
|
||||
message.success(discount === 100 ? '已清除价格浮动' : '价格已更新');
|
||||
}
|
||||
|
||||
function clearCartPriceDiscount() {
|
||||
applyCartPriceDiscount(100);
|
||||
}
|
||||
|
||||
const priceDiscountLabel = computed(() => formatPriceDiscountLabel(priceDiscount.value));
|
||||
|
||||
function openOrderPriceAdjust() {
|
||||
if (!priceAdjustEnabled.value || !currentDrugs.value.length) return;
|
||||
currentDrugs.value = snapshotDrugOrigins(currentDrugs.value);
|
||||
const discount = Number(priceDiscount.value);
|
||||
priceAdjustDrawerApi.setData({
|
||||
stage: 'pre_send',
|
||||
priceDiscount: discount,
|
||||
price_discount: discount,
|
||||
previewOriginPrice: 100,
|
||||
quickOptions: normalizeQuickOptions(priceAdjustConfig.value.order_discount_quick_options),
|
||||
onApplyPreSend: (payload: { priceDiscount: number }) => applyCartPriceDiscount(payload.priceDiscount),
|
||||
});
|
||||
priceAdjustDrawerApi.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除药品(本地)
|
||||
* @param index
|
||||
@@ -706,6 +761,7 @@ const sendPrescription = () => {
|
||||
// 是否二次签名
|
||||
doctor_second_sign: doctorSecondSign.value,
|
||||
salesperson_transfer_prescription_id: salespersonTransferPrescriptionId.value || undefined,
|
||||
price_discount: priceDiscount.value,
|
||||
}).then((res) => {
|
||||
message.success('处方已发送');
|
||||
|
||||
@@ -2795,7 +2851,6 @@ watch(
|
||||
</div>
|
||||
<span>{{ drug.price }}</span>
|
||||
<div>
|
||||
<!-- 西药编辑/保存按钮 -->
|
||||
<Button
|
||||
v-if="activeCategory !== 1 && !drug.isEditing"
|
||||
type="link"
|
||||
@@ -3001,7 +3056,16 @@ watch(
|
||||
<div class="price-box">
|
||||
<div class="total-cost">加工费:¥{{ processingFee.toFixed(2) }}</div>
|
||||
<div class="total-cost">
|
||||
商品价格:¥{{ totalProductCost.toFixed(2) }}
|
||||
商品价格:
|
||||
<Button
|
||||
v-if="priceAdjustEnabled && currentDrugs.length"
|
||||
type="link"
|
||||
class="!p-0"
|
||||
@click="openOrderPriceAdjust"
|
||||
>
|
||||
¥{{ totalProductCost.toFixed(2) }}
|
||||
</Button>
|
||||
<template v-else>¥{{ totalProductCost.toFixed(2) }}</template>
|
||||
<span
|
||||
v-if="chineseGrossMarginPercent !== null"
|
||||
class="ml-3 text-orange-600"
|
||||
@@ -3011,6 +3075,13 @@ watch(
|
||||
</div>
|
||||
<div class="total-cost">总计:¥{{ totalCost.toFixed(2) }}</div>
|
||||
|
||||
<div v-if="priceAdjustEnabled && currentDrugs.length" class="mt-2 text-sm text-gray-600">
|
||||
当前浮动:{{ priceDiscountLabel || '无' }}
|
||||
<Button v-if="priceDiscount !== 100" type="link" size="small" @click="clearCartPriceDiscount">
|
||||
清除浮动
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
v-if="activeCategory === 1"
|
||||
class="mt-5 w-full"
|
||||
@@ -3043,6 +3114,7 @@ watch(
|
||||
<CommonPrescriptionModals/>
|
||||
<!-- 确认添加药品弹窗 -->
|
||||
<ConfirmModalComponent />
|
||||
<PriceAdjustDrawer />
|
||||
<AntModal title="二次签名" v-model:open="doctorSecondSignModal" @ok="doctorSecondSignModalOk">
|
||||
<div class="doctor-second-sign-title">有毒:</div>
|
||||
<p v-for="item in checkData.message.poisonous || []">【{{ item }}】</p>
|
||||
|
||||
@@ -6,6 +6,10 @@ import { useVbenDrawer } from '@vben/common-ui';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getPatientOrders } from '../api/index.ts';
|
||||
import PriceAdjustModal from './PriceAdjustModal.vue';
|
||||
import OrderPricePercentAdjustDrawer from '#/views/business/order/components/OrderPricePercentAdjustDrawer.vue';
|
||||
import { getOrderPriceAdjustConfig, adjustOrderPercent } from '#/api/order/priceAdjust';
|
||||
import type { QuickDiscountOption } from '#/utils/pricePercentAdjust';
|
||||
import { normalizeQuickOptions } from '#/utils/pricePercentAdjust';
|
||||
|
||||
defineOptions({
|
||||
name: 'PatientOrderListModal',
|
||||
@@ -21,6 +25,7 @@ interface Product {
|
||||
is_chinese_medicine: boolean;
|
||||
min_adjust_price: number | null;
|
||||
can_adjust: boolean;
|
||||
can_percent_adjust?: boolean;
|
||||
drug_image?: string;
|
||||
specification?: string;
|
||||
source_name?: string;
|
||||
@@ -36,6 +41,8 @@ interface Order {
|
||||
status: number;
|
||||
is_pay: number;
|
||||
created_at: string;
|
||||
price_discount?: number;
|
||||
price_discount_label?: string | null;
|
||||
products: Product[];
|
||||
}
|
||||
|
||||
@@ -62,6 +69,15 @@ const [PriceAdjustDrawerComponent, priceAdjustDrawerApi] = useVbenDrawer({
|
||||
connectedComponent: PriceAdjustModal,
|
||||
});
|
||||
|
||||
const [PercentAdjustDrawer, percentAdjustDrawerApi] = useVbenDrawer({
|
||||
connectedComponent: OrderPricePercentAdjustDrawer,
|
||||
});
|
||||
|
||||
const priceAdjustMeta = ref({
|
||||
scope: 'sale_only' as 'both' | 'sale_only',
|
||||
quickOptions: [] as QuickDiscountOption[],
|
||||
});
|
||||
|
||||
const registerId = ref<number | null>(null);
|
||||
|
||||
const gridOptions: VxeGridProps<Order> = {
|
||||
@@ -166,12 +182,44 @@ const handleAdjustPrice = (product: Product, orderId: number) => {
|
||||
priceAdjustDrawerApi.open();
|
||||
};
|
||||
|
||||
const handleOrderPercentAdjust = (order: Order) => {
|
||||
const discount = Number(order.price_discount ?? 100);
|
||||
percentAdjustDrawerApi.setData({
|
||||
stage: 'post_order',
|
||||
priceDiscount: discount,
|
||||
price_discount: discount,
|
||||
productOrderId: order.id,
|
||||
registerId: registerId.value,
|
||||
quickOptions: normalizeQuickOptions(priceAdjustMeta.value.quickOptions),
|
||||
onSuccess: () => fetchOrders(),
|
||||
});
|
||||
percentAdjustDrawerApi.open();
|
||||
};
|
||||
|
||||
function clearOrderPriceDiscount(order: Order) {
|
||||
adjustOrderPercent({
|
||||
product_order_id: order.id,
|
||||
price_discount: 100,
|
||||
register_id: registerId.value || 0,
|
||||
})
|
||||
.then(() => {
|
||||
message.success('已清除价格浮动');
|
||||
fetchOrders();
|
||||
})
|
||||
.catch((e: any) => message.error(e?.message || '操作失败'));
|
||||
}
|
||||
|
||||
const fetchOrders = async () => {
|
||||
if (!registerId.value) {
|
||||
return;
|
||||
}
|
||||
gridApi.setLoading(true);
|
||||
try {
|
||||
const cfg = await getOrderPriceAdjustConfig();
|
||||
priceAdjustMeta.value = {
|
||||
scope: cfg?.order_price_adjust_scope === 'both' ? 'both' : 'sale_only',
|
||||
quickOptions: normalizeQuickOptions(cfg?.order_discount_quick_options),
|
||||
};
|
||||
const res = await getPatientOrders(registerId.value);
|
||||
const orderList: Order[] = res?.result || res?.data || res || [];
|
||||
gridApi.setGridOptions({ data: orderList });
|
||||
@@ -218,13 +266,24 @@ const fetchOrders = async () => {
|
||||
|
||||
<!-- 操作 -->
|
||||
<template #action="{ row }">
|
||||
<span style="color: #999">共 {{ row.products?.length || 0 }} 个商品</span>
|
||||
<div v-if="row.price_discount_label" class="text-xs text-gray-500 mb-1">
|
||||
浮动:{{ row.price_discount_label }}
|
||||
</div>
|
||||
<Button
|
||||
v-if="row.is_pay !== 1 && row.price_discount && row.price_discount !== 100 && row.products?.some((p: Product) => p.can_percent_adjust)"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="clearOrderPriceDiscount(row)"
|
||||
>
|
||||
清除浮动
|
||||
</Button>
|
||||
<span v-if="row.is_pay === 1 || !row.products?.some((p: Product) => p.can_percent_adjust)" style="color: #999">共 {{ row.products?.length || 0 }} 个商品</span>
|
||||
</template>
|
||||
|
||||
<!-- 展开内容:商品列表 -->
|
||||
<template #expand-content="{ row }">
|
||||
<div
|
||||
v-if="row.prescription_type === 2 || row.prescription_type === 5"
|
||||
v-if="row.prescription_type === 2 || row.prescription_type === 5 || row.products?.some((p: Product) => p.can_percent_adjust)"
|
||||
class="product-list mt-5"
|
||||
>
|
||||
<div
|
||||
@@ -268,28 +327,20 @@ const fetchOrders = async () => {
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<span class="text-gray-500 dark:text-gray-400">商品价格:</span>
|
||||
<a
|
||||
v-if="!product.is_chinese_medicine && product.can_adjust"
|
||||
class="text-primary cursor-pointer hover:underline ml-1"
|
||||
@click="handleAdjustPrice(product, row.id)"
|
||||
>
|
||||
¥{{ Number(product.price || 0).toFixed(4) }}
|
||||
</a>
|
||||
<span v-else>¥{{ Number(product.price || 0).toFixed(4) }}</span>
|
||||
<span class="ml-1">¥{{ Number(product.price || 0).toFixed(4) }}</span>
|
||||
</div>
|
||||
<div v-if="!product.is_chinese_medicine && product.can_adjust" class="text-sm mt-2">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleAdjustPrice(product, row.id)"
|
||||
>
|
||||
<div
|
||||
v-if="!product.is_chinese_medicine && product.can_adjust && row.is_pay !== 1"
|
||||
class="text-sm mt-2"
|
||||
>
|
||||
<Button type="link" size="small" @click="handleAdjustPrice(product, row.id)">
|
||||
调整价格
|
||||
</Button>
|
||||
<span class="text-gray-500 dark:text-gray-400 ml-2">
|
||||
(最低调整金额: ¥{{ Number(product.min_adjust_price || 0).toFixed(2) }})
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="product.is_chinese_medicine" class="text-sm">
|
||||
<div v-if="product.is_chinese_medicine && !product.can_percent_adjust" class="text-sm">
|
||||
<span class="text-gray-500 dark:text-gray-400">备注:</span>
|
||||
<span style="color: #999">中药不可调整</span>
|
||||
</div>
|
||||
@@ -316,6 +367,7 @@ const fetchOrders = async () => {
|
||||
</template>
|
||||
</Grid>
|
||||
<PriceAdjustDrawerComponent />
|
||||
<PercentAdjustDrawer />
|
||||
</DrawerComponent>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,11 +1,33 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
const prefix = 'log/';
|
||||
|
||||
export type ApiLogUserSearchItem = {
|
||||
id: number;
|
||||
name: string;
|
||||
avatar: string;
|
||||
role_label: string;
|
||||
user_type: 'admin' | 'patient' | 'doctor';
|
||||
platform_type: 0 | 1 | 2;
|
||||
};
|
||||
|
||||
/**
|
||||
* 分页查询用户列表
|
||||
* @param data
|
||||
* 分页查询 Api 访问日志
|
||||
*/
|
||||
export async function getOldApiLogList(data: any) {
|
||||
return requestClient.get<any>(`${prefix}api-list`, { params: data });
|
||||
}
|
||||
// Api访问日志
|
||||
|
||||
/**
|
||||
* Api 访问日志操作用户搜索
|
||||
*/
|
||||
export async function searchApiLogUsers(params: {
|
||||
user_type: 'admin' | 'patient' | 'doctor';
|
||||
keyword: string;
|
||||
pageSize?: number;
|
||||
}) {
|
||||
return requestClient.get<{ items: ApiLogUserSearchItem[] }>(
|
||||
`${prefix}user-search`,
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,10 +9,9 @@ export const formOptions: VbenFormProps = {
|
||||
collapsed: false,
|
||||
schema: [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
// defaultValue: [dayjs().startOf('month'), dayjs()],
|
||||
fieldName: 'user_id',
|
||||
label: '用户ID',
|
||||
component: 'ApiLogUserPicker',
|
||||
fieldName: 'user_filter',
|
||||
label: '操作用户',
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
|
||||
@@ -54,10 +54,13 @@ export const gridOptions: VxeGridProps<RowType> = {
|
||||
ajax: {
|
||||
// 请求后端接口方法
|
||||
query: async ({ page }, formValues) => {
|
||||
const { user_filter, platform_type, ...rest } = formValues;
|
||||
return await getOldApiLogList({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
...rest,
|
||||
user_id: user_filter?.id,
|
||||
platform_type: platform_type ?? user_filter?.platform_type,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -19,10 +19,30 @@ import { getIcon } from '#/util/tool';
|
||||
import {Icon} from "#/components/icon";
|
||||
|
||||
ref(false);
|
||||
|
||||
let gridApiRef: ReturnType<typeof useVbenVxeGrid>[1];
|
||||
|
||||
function syncPlatformType(platformType: 0 | 1 | 2) {
|
||||
gridApiRef?.formApi?.setFieldValue('platform_type', platformType);
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
formOptions: {
|
||||
...formOptions,
|
||||
schema: formOptions.schema?.map((item) =>
|
||||
item.fieldName === 'user_filter'
|
||||
? {
|
||||
...item,
|
||||
componentProps: {
|
||||
onPlatformTypeChange: syncPlatformType,
|
||||
},
|
||||
}
|
||||
: item,
|
||||
),
|
||||
},
|
||||
gridOptions,
|
||||
});
|
||||
gridApiRef = gridApi;
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: FormModalDemo,
|
||||
|
||||
@@ -119,6 +119,19 @@ export const modalFormProps: VbenFormProps = {
|
||||
defaultValue: 0, // 默认订阅
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
formItemClass: 'col-span-6',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '关闭', value: 0 },
|
||||
{ label: '开启', value: 1 },
|
||||
],
|
||||
},
|
||||
fieldName: 'enable_order_price_percent_adjust',
|
||||
label: '订单打折/涨幅',
|
||||
defaultValue: 0,
|
||||
},
|
||||
{
|
||||
// 联系人姓名字段
|
||||
component: 'VbenInput',
|
||||
@@ -142,14 +155,10 @@ export const modalFormProps: VbenFormProps = {
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
// 业务码字段
|
||||
component: 'VbenInput',
|
||||
component: 'PromoterPicker',
|
||||
formItemClass: 'col-span-6',
|
||||
componentProps: {
|
||||
placeholder: '请输入业务码',
|
||||
},
|
||||
fieldName: 'code',
|
||||
label: '业务码',
|
||||
label: '业务员',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
// 直接使用store的API接口
|
||||
import { getStoreList } from '#/views/system/store/api';
|
||||
|
||||
// 表格行数据类型定义
|
||||
interface RowType {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -12,29 +10,27 @@ interface RowType {
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// 药店管理表格配置
|
||||
// 注意:查询时会自动添加type=1参数,只显示药店类型的数据
|
||||
export const gridOptions: VxeGridProps<RowType> = {
|
||||
// 复选框配置
|
||||
checkboxConfig: {
|
||||
highlight: true,
|
||||
labelField: '',
|
||||
},
|
||||
// 列配置
|
||||
columnConfig: {
|
||||
useKey: true,
|
||||
},
|
||||
// 行配置
|
||||
rowConfig: {
|
||||
useKey: true,
|
||||
},
|
||||
// 表格列定义
|
||||
columns: [
|
||||
{ type: 'checkbox', width: 60 }, // 复选框列
|
||||
{ field: 'id', align: 'left', title: 'ID', width: 100 }, // ID列
|
||||
{ field: 'name', align: 'left', title: '药店名称', width: 140 }, // 药店名称列(已从"诊所名称"改为"药店名称")
|
||||
{ type: 'checkbox', width: 60 },
|
||||
{
|
||||
field: 'store_basic_info',
|
||||
align: 'left',
|
||||
title: '基础信息',
|
||||
width: 280,
|
||||
slots: { default: 'store_basic_info' },
|
||||
},
|
||||
{
|
||||
// 在线复诊配置列(合并委托诊所和负责人)
|
||||
field: 'online_consultation_config',
|
||||
align: 'left',
|
||||
title: '在线复诊配置',
|
||||
@@ -42,69 +38,37 @@ export const gridOptions: VxeGridProps<RowType> = {
|
||||
slots: { default: 'online_consultation_config' },
|
||||
},
|
||||
{
|
||||
field: 'is_shipping_free',
|
||||
field: 'store_config_1',
|
||||
align: 'left',
|
||||
title: '是否包邮',
|
||||
slots: { default: 'is_shipping_free' },
|
||||
width: 120,
|
||||
title: '开关配置',
|
||||
width: 330,
|
||||
slots: { default: 'store_config_1' },
|
||||
},
|
||||
{
|
||||
field: 'bank_card_report_status',
|
||||
field: 'store_config_2',
|
||||
align: 'left',
|
||||
title: '银行卡报备',
|
||||
slots: { default: 'bank_card_report_status' },
|
||||
width: 120,
|
||||
title: '业务配置',
|
||||
width: 330,
|
||||
slots: { default: 'store_config_2' },
|
||||
},
|
||||
{
|
||||
// 订阅价格波动列(显示为开关)
|
||||
field: 'subscribe_price_change',
|
||||
align: 'left',
|
||||
title: '订阅价格',
|
||||
slots: { default: 'subscribe_price_change' },
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
field: 'see_rate',
|
||||
align: 'left',
|
||||
title: '查看毛利率',
|
||||
slots: { default: 'see_rate' },
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
// 药店二维码列(已从"诊所二维码"改为"药店二维码")
|
||||
field: 'qr_code',
|
||||
align: 'left',
|
||||
title: '药店二维码',
|
||||
slots: { default: 'qr_code' },
|
||||
width: 130,
|
||||
},
|
||||
{ field: 'position', title: '详细地址', width: 140 },
|
||||
{ field: 'new_admin.nick_name', title: '业务员', slots: { default: 'sales_admin' }, width: 160 },
|
||||
{ field: 'mobile', title: '联系人、电话', slots: { default: 'mobile' }, width: 140 },
|
||||
{
|
||||
field: 'erp_id',
|
||||
align: 'left',
|
||||
title: 'ERP ID',
|
||||
width: 140,
|
||||
slots: { default: 'erp_id_cell' },
|
||||
},
|
||||
{
|
||||
field: 'mes_id',
|
||||
align: 'left',
|
||||
title: 'MES ID',
|
||||
width: 140,
|
||||
slots: { default: 'mes_id_cell' },
|
||||
},
|
||||
{ field: 'position', title: '详细地址', width: 140 }, // 详细地址列
|
||||
{ field: 'new_admin.nick_name', title: '业务员', width: 140 }, // 业务员列
|
||||
{ field: 'mobile', title: '联系人、电话', slots: { default: 'mobile' }, width: 140 }, // 联系人电话列
|
||||
{
|
||||
// 营业时间列
|
||||
field: 'start_time',
|
||||
title: '营业时间',
|
||||
width: 140,
|
||||
slots: { default: 'start-time' },
|
||||
},
|
||||
{ field: 'created_at', title: '注册时间', width: 140 }, // 注册时间列
|
||||
{ field: 'created_at', title: '注册时间', width: 140 },
|
||||
{
|
||||
// 操作列
|
||||
type: 'html',
|
||||
title: '操作',
|
||||
width: 200,
|
||||
@@ -113,18 +77,14 @@ export const gridOptions: VxeGridProps<RowType> = {
|
||||
},
|
||||
],
|
||||
keepSource: true,
|
||||
// 分页配置
|
||||
pagerConfig: {},
|
||||
// 代理配置(数据请求)
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
// 请求后端接口方法
|
||||
// 注意:会自动添加type=1参数,只查询药店类型的数据
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getStoreList({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
type: 1, // 固定为药店类型
|
||||
type: 1,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
@@ -132,20 +92,17 @@ export const gridOptions: VxeGridProps<RowType> = {
|
||||
},
|
||||
height: 'auto',
|
||||
border: false,
|
||||
// 工具栏配置
|
||||
toolbarConfig: {
|
||||
// 是否显示搜索表单控制按钮
|
||||
// @ts-ignore 正式环境时有完整的类型声明
|
||||
search: true,
|
||||
refresh: true, // 刷新按钮
|
||||
print: false, // 打印按钮
|
||||
export: false, // 导出按钮
|
||||
zoom: true, // 最大化最小化按钮
|
||||
refresh: true,
|
||||
print: false,
|
||||
export: false,
|
||||
zoom: true,
|
||||
slots: {
|
||||
buttons: 'toolbar-buttons', // 工具栏按钮插槽
|
||||
buttons: 'toolbar-buttons',
|
||||
},
|
||||
custom: {
|
||||
// 自定义列-图标
|
||||
icon: 'vxe-icon-menu',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -8,11 +8,12 @@ import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { Button, Image, message, Switch, Tag } from 'ant-design-vue';
|
||||
import { Button, Image, message, Tag } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { Icon } from '#/components/icon';
|
||||
import QrCodePreview from '#/components/modal/QrCodePreview.vue';
|
||||
import PromoterInfoCard from '#/components/promoter/PromoterInfoCard.vue';
|
||||
import { TableAction } from '#/components/table-action';
|
||||
|
||||
// 导入药店管理相关的API(直接使用store的API)
|
||||
@@ -24,9 +25,13 @@ import {
|
||||
updateStoreShippingFree,
|
||||
updateStoreSeeRateStatus,
|
||||
updateStoreSubscribeStatus,
|
||||
updateStoreOrderPricePercentAdjust,
|
||||
updateAllowInsuranceCategoryApi,
|
||||
toggleSalespersonSeePriceApi,
|
||||
} from '#/views/system/store/api';
|
||||
// 导入表单弹窗组件
|
||||
import FormModalDemo from './components/modal.vue';
|
||||
import StoreBasicInfoCell from '#/views/system/store/components/cells/StoreBasicInfoCell.vue';
|
||||
import StoreConfigTogglesCell from '#/views/system/store/components/cells/StoreConfigTogglesCell.vue';
|
||||
// 导入绑定在线复诊配置弹窗(使用store的组件)
|
||||
import BindConsultationModal from '#/views/system/store/components/BindConsultationModal.vue';
|
||||
import DrugPriceModal from '#/views/system/store/components/DrugPriceModal.vue';
|
||||
@@ -299,6 +304,27 @@ const updateSeeRate = (id: number) => {
|
||||
});
|
||||
};
|
||||
|
||||
const updateOrderPricePercentAdjust = (id: number) => {
|
||||
updateStoreOrderPricePercentAdjust({ id }).then(() => {
|
||||
message.success('修改成功!');
|
||||
gridApi.query();
|
||||
});
|
||||
};
|
||||
|
||||
const updateAllowInsurance = (id: number) => {
|
||||
updateAllowInsuranceCategoryApi({ id }).then(() => {
|
||||
message.success('修改成功!');
|
||||
gridApi.query();
|
||||
});
|
||||
};
|
||||
|
||||
const updateSalespersonSeePrice = (storeId: number) => {
|
||||
toggleSalespersonSeePriceApi({ store_id: storeId }).then(() => {
|
||||
message.success('修改成功!');
|
||||
gridApi.query();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量同步总仓库药品
|
||||
*/
|
||||
@@ -379,6 +405,47 @@ const batchSyncDrugPrice = () => {
|
||||
</template>
|
||||
</TableAction>
|
||||
</template>
|
||||
<template #store_basic_info="{ row }">
|
||||
<StoreBasicInfoCell
|
||||
:row="row"
|
||||
variant="pharmacy"
|
||||
:on-copy="copyText"
|
||||
:on-edit-external-field="showStoreExternalFieldModal"
|
||||
/>
|
||||
</template>
|
||||
<template #store_config_1="{ row }">
|
||||
<StoreConfigTogglesCell
|
||||
:row="row"
|
||||
section="switches"
|
||||
:get-bank-card-report-tag-color="getBankCardReportTagColor"
|
||||
:get-bank-card-report-tag-text="getBankCardReportTagText"
|
||||
:on-shipping-free="updateShippingFree"
|
||||
:on-subscribe="updateSubscribe"
|
||||
:on-see-rate="updateSeeRate"
|
||||
:on-order-price-adjust="updateOrderPricePercentAdjust"
|
||||
:on-bank-card-report="handleBankCardReportColumnClick"
|
||||
/>
|
||||
</template>
|
||||
<template #store_config_2="{ row }">
|
||||
<StoreConfigTogglesCell
|
||||
:row="row"
|
||||
section="business"
|
||||
show-insurance
|
||||
show-salesperson-see-price
|
||||
:get-bank-card-report-tag-color="getBankCardReportTagColor"
|
||||
:get-bank-card-report-tag-text="getBankCardReportTagText"
|
||||
:on-shipping-free="updateShippingFree"
|
||||
:on-subscribe="updateSubscribe"
|
||||
:on-see-rate="updateSeeRate"
|
||||
:on-order-price-adjust="updateOrderPricePercentAdjust"
|
||||
:on-bank-card-report="handleBankCardReportColumnClick"
|
||||
:on-allow-insurance="updateAllowInsurance"
|
||||
:on-salesperson-see-price="updateSalespersonSeePrice"
|
||||
/>
|
||||
</template>
|
||||
<template #sales_admin="{ row }">
|
||||
<PromoterInfoCard :admin="row.new_admin" size="compact" />
|
||||
</template>
|
||||
<template #mobile="{ row }">
|
||||
<div>
|
||||
药店联系人:{{ row.contact }}
|
||||
@@ -406,26 +473,6 @@ const batchSyncDrugPrice = () => {
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #erp_id_cell="{ row }">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
class="h-auto p-0"
|
||||
@click="showStoreExternalFieldModal(row, 'erp_id', 'ERP ID')"
|
||||
>
|
||||
{{ row.erp_id != null && row.erp_id !== '' ? row.erp_id : '-' }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #mes_id_cell="{ row }">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
class="h-auto p-0"
|
||||
@click="showStoreExternalFieldModal(row, 'mes_id', 'MES ID')"
|
||||
>
|
||||
{{ row.mes_id != null && row.mes_id !== '' ? row.mes_id : '-' }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #online_consultation_config="{ row }">
|
||||
<div
|
||||
v-if="row.is_internet_medical === 1 && row.online_consultation_doctor_name"
|
||||
@@ -446,43 +493,6 @@ const batchSyncDrugPrice = () => {
|
||||
绑定
|
||||
</Button>
|
||||
</template>
|
||||
<template #is_shipping_free="{ row }">
|
||||
<Switch
|
||||
:checked="row.is_shipping_free"
|
||||
:checked-value="1"
|
||||
:un-checked-value="0"
|
||||
checked-children="包邮"
|
||||
un-checked-children="不包邮"
|
||||
@click="updateShippingFree(row.id)"
|
||||
/>
|
||||
</template>
|
||||
<template #bank_card_report_status="{ row }">
|
||||
<Tag
|
||||
:color="getBankCardReportTagColor(row.bank_card_report_status)"
|
||||
style="cursor: pointer"
|
||||
@click="handleBankCardReportColumnClick(row)"
|
||||
>
|
||||
{{ getBankCardReportTagText(row) }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template #subscribe_price_change="{ row }">
|
||||
<Tag
|
||||
:color="row.subscribe_price_change === 0 ? 'success' : 'default'"
|
||||
style="cursor: pointer"
|
||||
@click="updateSubscribe(row.id)"
|
||||
>
|
||||
{{ row.subscribe_price_change === 0 ? '订阅' : '不订阅' }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template #see_rate="{ row }">
|
||||
<Tag
|
||||
:color="Number(row.see_rate) === 1 ? 'success' : 'default'"
|
||||
style="cursor: pointer"
|
||||
@click="updateSeeRate(row.id)"
|
||||
>
|
||||
{{ Number(row.see_rate) === 1 ? '可查看' : '不可查看' }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template #start-time="{ row }">
|
||||
早:<Tag color="success">{{ row.start_time }}</Tag>
|
||||
<br />
|
||||
|
||||
@@ -44,11 +44,25 @@ export async function getPromoterOptions() {
|
||||
nick_name: string;
|
||||
avatar: string;
|
||||
code: string;
|
||||
phone: string;
|
||||
role_id: number;
|
||||
role_name: string;
|
||||
}[]>(`${prefix}promoter-options`);
|
||||
}
|
||||
|
||||
export async function getPromoterDetailApi(params: { id?: number; code?: string }) {
|
||||
return requestClient.get<{
|
||||
id: number;
|
||||
nick_name: string;
|
||||
avatar: string;
|
||||
code: string;
|
||||
phone: string;
|
||||
role_name: string;
|
||||
clinic_input_count: number;
|
||||
pharmacy_input_count: number;
|
||||
}>(`${prefix}promoter-detail`, { params });
|
||||
}
|
||||
|
||||
export async function auditStoreInput(data: {
|
||||
id: number;
|
||||
status: number; // 1=审核通过,2=审核拒绝
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Avatar, Button, message, Modal, Tag, Textarea } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { TableAction } from '#/components/table-action';
|
||||
import PromoterInfoCard from '#/components/promoter/PromoterInfoCard.vue';
|
||||
|
||||
import { auditStoreInput, getStoreInputDetail } from './api';
|
||||
import AuditModal from './components/AuditModal.vue';
|
||||
@@ -132,6 +133,9 @@ const getStatusText = (status: number) => {
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template #promoter_admin="{ row }">
|
||||
<PromoterInfoCard :admin="row.promoter_admin" size="compact" />
|
||||
</template>
|
||||
<template #audit_time="{ row }">
|
||||
<span v-if="row.audit_time">
|
||||
{{ new Date(row.audit_time * 1000).toLocaleString() }}
|
||||
|
||||
@@ -66,6 +66,12 @@ export const inputGridOptions: VxeGridProps<RowType> = {
|
||||
minWidth: 140,
|
||||
slots: { default: 'inputUser' },
|
||||
},
|
||||
{
|
||||
field: 'promoter_admin',
|
||||
title: '业务员',
|
||||
minWidth: 160,
|
||||
slots: { default: 'promoter_admin' },
|
||||
},
|
||||
{
|
||||
type: 'html',
|
||||
title: '操作',
|
||||
@@ -145,6 +151,12 @@ export const auditGridOptions: VxeGridProps<RowType> = {
|
||||
slots: { default: 'inputUser' },
|
||||
},
|
||||
{ field: 'auditAdmin.nick_name', title: '审核人' },
|
||||
{
|
||||
field: 'promoter_admin',
|
||||
title: '业务员',
|
||||
minWidth: 160,
|
||||
slots: { default: 'promoter_admin' },
|
||||
},
|
||||
{ field: 'audit_time', title: '审核时间', slots: { default: 'audit_time' } },
|
||||
{ field: 'contact', title: '联系人' },
|
||||
{ field: 'mobile', title: '联系电话' },
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Avatar, Button, message, Tag } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { TableAction } from '#/components/table-action';
|
||||
import PromoterInfoCard from '#/components/promoter/PromoterInfoCard.vue';
|
||||
|
||||
import {
|
||||
createStoreInput,
|
||||
@@ -154,6 +155,9 @@ const getStatusText = (status: number) => {
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template #promoter_admin="{ row }">
|
||||
<PromoterInfoCard :admin="row.promoter_admin" size="compact" />
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
|
||||
@@ -170,6 +170,10 @@ export async function updateAllowInsuranceCategoryApi(data: { id: number }) {
|
||||
return requestClient.post<any>(`${prefix}update-allow-insurance-category`, data);
|
||||
}
|
||||
|
||||
export async function updateStoreOrderPricePercentAdjust(data: { id: number }) {
|
||||
return requestClient.post<any>(`${prefix}update-order-price-percent-adjust`, data);
|
||||
}
|
||||
|
||||
export async function toggleSalespersonSeePriceApi(data: { store_id: number }) {
|
||||
return requestClient.post<any>('salesperson-store-config/toggle-see-price', data);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
<script lang="ts" setup>
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
defineProps<{
|
||||
row: Record<string, any>;
|
||||
variant: 'clinic' | 'pharmacy';
|
||||
onCopy: (text: string | number) => void;
|
||||
onEditExternalField: (
|
||||
row: Record<string, any>,
|
||||
field: 'erp_id' | 'mes_id',
|
||||
label: string,
|
||||
) => void;
|
||||
}>();
|
||||
|
||||
function getClinicTypeText(clinicType: number) {
|
||||
if (clinicType === 1) return '西医诊所';
|
||||
if (clinicType === 2) return '中医诊所';
|
||||
return '未设置';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="store-basic-info">
|
||||
<div class="store-basic-info__name">{{ row.name || '-' }}</div>
|
||||
<div class="store-basic-info__grid">
|
||||
<div class="info-item">
|
||||
<span class="info-item__label">ID</span>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
class="info-item__action"
|
||||
@click="onCopy(row.id)"
|
||||
>
|
||||
{{ row.id }}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-item__label">ERP</span>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
class="info-item__action"
|
||||
@click="onEditExternalField(row, 'erp_id', 'ERP ID')"
|
||||
>
|
||||
{{ row.erp_id != null && row.erp_id !== '' ? row.erp_id : '-' }}
|
||||
</Button>
|
||||
</div>
|
||||
<template v-if="variant === 'clinic'">
|
||||
<div class="info-item">
|
||||
<span class="info-item__label">诊所类型</span>
|
||||
<span class="info-item__value">{{ getClinicTypeText(Number(row.clinic_type)) }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-item__label">管理员</span>
|
||||
<template v-if="row.store_admin?.phone">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
class="info-item__action"
|
||||
@click="onCopy(row.store_admin.phone)"
|
||||
>
|
||||
{{ row.store_admin.phone }}
|
||||
</Button>
|
||||
</template>
|
||||
<span v-else class="info-item__value">-</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="info-item">
|
||||
<span class="info-item__label">MES</span>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
class="info-item__action"
|
||||
@click="onEditExternalField(row, 'mes_id', 'MES ID')"
|
||||
>
|
||||
{{ row.mes_id != null && row.mes_id !== '' ? row.mes_id : '-' }}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="info-item info-item--empty" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.store-basic-info {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.store-basic-info__name {
|
||||
font-weight: 500;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.store-basic-info__grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4px 8px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
color: rgb(107 114 128);
|
||||
}
|
||||
|
||||
.info-item--empty {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.info-item__label {
|
||||
flex-shrink: 0;
|
||||
min-width: 48px;
|
||||
color: rgb(156 163 175);
|
||||
}
|
||||
|
||||
.info-item__value {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.info-item__action {
|
||||
height: auto;
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,244 @@
|
||||
<script lang="ts" setup>
|
||||
import { Switch, Tag } from 'ant-design-vue';
|
||||
|
||||
defineProps<{
|
||||
row: Record<string, any>;
|
||||
section: 'switches' | 'business' | 'all';
|
||||
showClinicType?: boolean;
|
||||
showInsurance?: boolean;
|
||||
showSalespersonSeePrice?: boolean;
|
||||
getBankCardReportTagColor: (status: number) => string;
|
||||
getBankCardReportTagText: (row: Record<string, any>) => string;
|
||||
onShippingFree: (id: number) => void;
|
||||
onSubscribe: (id: number) => void;
|
||||
onSeeRate: (id: number) => void;
|
||||
onOrderPriceAdjust: (id: number) => void;
|
||||
onBankCardReport: (row: Record<string, any>) => void;
|
||||
onClinicType?: (row: Record<string, any>) => void;
|
||||
onAllowInsurance?: (id: number) => void;
|
||||
onSalespersonSeePrice?: (id: number) => void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="store-config-toggles">
|
||||
<template v-if="section === 'switches'">
|
||||
<div class="config-item">
|
||||
<span class="config-item__label">是否包邮</span>
|
||||
<div class="config-item__control">
|
||||
<Switch
|
||||
:checked="row.is_shipping_free"
|
||||
:checked-value="1"
|
||||
:un-checked-value="0"
|
||||
checked-children="包邮"
|
||||
un-checked-children="不包邮"
|
||||
size="small"
|
||||
@click="onShippingFree(row.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="config-item__label">订阅价格</span>
|
||||
<div class="config-item__control">
|
||||
<Tag
|
||||
:color="row.subscribe_price_change === 0 ? 'success' : 'default'"
|
||||
class="config-item__tag"
|
||||
@click="onSubscribe(row.id)"
|
||||
>
|
||||
{{ row.subscribe_price_change === 0 ? '订阅' : '不订阅' }}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="config-item__label">查看毛利率</span>
|
||||
<div class="config-item__control">
|
||||
<Tag
|
||||
:color="Number(row.see_rate) === 1 ? 'success' : 'default'"
|
||||
class="config-item__tag"
|
||||
@click="onSeeRate(row.id)"
|
||||
>
|
||||
{{ Number(row.see_rate) === 1 ? '可查看' : '不可查看' }}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="config-item__label">订单打折</span>
|
||||
<div class="config-item__control">
|
||||
<Tag
|
||||
:color="Number(row.enable_order_price_percent_adjust) === 1 ? 'success' : 'default'"
|
||||
class="config-item__tag"
|
||||
@click="onOrderPriceAdjust(row.id)"
|
||||
>
|
||||
{{ Number(row.enable_order_price_percent_adjust) === 1 ? '开启' : '关闭' }}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="section === 'business'">
|
||||
<div v-if="showClinicType && onClinicType" class="config-item">
|
||||
<span class="config-item__label">诊所类型</span>
|
||||
<div class="config-item__control">
|
||||
<Tag
|
||||
:color="row.clinic_type === 1 ? 'blue' : row.clinic_type === 2 ? 'green' : 'default'"
|
||||
class="config-item__tag clinic-type-tag"
|
||||
@click="onClinicType(row)"
|
||||
>
|
||||
{{ row.clinic_type === 1 ? '西医诊所' : row.clinic_type === 2 ? '中医诊所' : '未设置' }}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="config-item__label">银行卡报备</span>
|
||||
<div class="config-item__control">
|
||||
<Tag
|
||||
:color="getBankCardReportTagColor(row.bank_card_report_status)"
|
||||
class="config-item__tag"
|
||||
@click="onBankCardReport(row)"
|
||||
>
|
||||
{{ getBankCardReportTagText(row) }}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showInsurance && onAllowInsurance" class="config-item">
|
||||
<span class="config-item__label">医保可选</span>
|
||||
<div class="config-item__control">
|
||||
<Tag
|
||||
:color="Number(row.allow_insurance_category) === 1 ? 'success' : 'default'"
|
||||
class="config-item__tag"
|
||||
@click="onAllowInsurance(row.id)"
|
||||
>
|
||||
{{ Number(row.allow_insurance_category) === 1 ? '可选医保' : '仅自费' }}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showSalespersonSeePrice && onSalespersonSeePrice" class="config-item">
|
||||
<span class="config-item__label">推广员可见价格</span>
|
||||
<div class="config-item__control">
|
||||
<Tag
|
||||
:color="Number(row.salesperson_see_price) === 1 ? 'success' : 'default'"
|
||||
class="config-item__tag"
|
||||
@click="onSalespersonSeePrice(row.id)"
|
||||
>
|
||||
{{ Number(row.salesperson_see_price) === 1 ? '可见' : '不可见' }}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="section === 'all'">
|
||||
<div class="config-item">
|
||||
<span class="config-item__label">是否包邮</span>
|
||||
<div class="config-item__control">
|
||||
<Switch
|
||||
:checked="row.is_shipping_free"
|
||||
:checked-value="1"
|
||||
:un-checked-value="0"
|
||||
checked-children="包邮"
|
||||
un-checked-children="不包邮"
|
||||
size="small"
|
||||
@click="onShippingFree(row.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="config-item__label">银行卡报备</span>
|
||||
<div class="config-item__control">
|
||||
<Tag
|
||||
:color="getBankCardReportTagColor(row.bank_card_report_status)"
|
||||
class="config-item__tag"
|
||||
@click="onBankCardReport(row)"
|
||||
>
|
||||
{{ getBankCardReportTagText(row) }}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="config-item__label">订阅价格</span>
|
||||
<div class="config-item__control">
|
||||
<Tag
|
||||
:color="row.subscribe_price_change === 0 ? 'success' : 'default'"
|
||||
class="config-item__tag"
|
||||
@click="onSubscribe(row.id)"
|
||||
>
|
||||
{{ row.subscribe_price_change === 0 ? '订阅' : '不订阅' }}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="config-item__label">查看毛利率</span>
|
||||
<div class="config-item__control">
|
||||
<Tag
|
||||
:color="Number(row.see_rate) === 1 ? 'success' : 'default'"
|
||||
class="config-item__tag"
|
||||
@click="onSeeRate(row.id)"
|
||||
>
|
||||
{{ Number(row.see_rate) === 1 ? '可查看' : '不可查看' }}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="config-item__label">订单打折</span>
|
||||
<div class="config-item__control">
|
||||
<Tag
|
||||
:color="Number(row.enable_order_price_percent_adjust) === 1 ? 'success' : 'default'"
|
||||
class="config-item__tag"
|
||||
@click="onOrderPriceAdjust(row.id)"
|
||||
>
|
||||
{{ Number(row.enable_order_price_percent_adjust) === 1 ? '开启' : '关闭' }}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.store-config-toggles {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px 12px;
|
||||
}
|
||||
|
||||
.config-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.config-item__label {
|
||||
flex-shrink: 0;
|
||||
min-width: 72px;
|
||||
color: rgb(156 163 175);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.config-item__control {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.config-item__tag {
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.clinic-type-tag {
|
||||
transition: all 0.2s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.clinic-type-tag:hover {
|
||||
opacity: 0.8;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.clinic-type-tag:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
</style>
|
||||
@@ -114,6 +114,19 @@ export const modalFormProps: VbenFormProps = {
|
||||
label: '是否包邮',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
formItemClass: 'col-span-6',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '关闭', value: 0 },
|
||||
{ label: '开启', value: 1 },
|
||||
],
|
||||
},
|
||||
fieldName: 'enable_order_price_percent_adjust',
|
||||
label: '订单打折/涨幅',
|
||||
defaultValue: 0,
|
||||
},
|
||||
{
|
||||
// 诊所类型(单选)
|
||||
component: 'RadioGroup',
|
||||
@@ -167,13 +180,10 @@ export const modalFormProps: VbenFormProps = {
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
component: 'PromoterPicker',
|
||||
formItemClass: 'col-span-6',
|
||||
componentProps: {
|
||||
placeholder: '请输入业务码',
|
||||
},
|
||||
fieldName: 'code',
|
||||
label: '业务码',
|
||||
label: '业务员',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -23,14 +23,12 @@ export const gridOptions: VxeGridProps<RowType> = {
|
||||
},
|
||||
columns: [
|
||||
{ type: 'checkbox', width: 60 },
|
||||
{ field: 'id', align: 'left', title: 'ID', width: 100 },
|
||||
{ field: 'name', align: 'left', title: '诊所名称', width: 140 },
|
||||
{
|
||||
field: 'clinic_type',
|
||||
field: 'store_basic_info',
|
||||
align: 'left',
|
||||
title: '诊所类型',
|
||||
width: 120,
|
||||
slots: { default: 'clinic_type' },
|
||||
title: '基础信息',
|
||||
width: 280,
|
||||
slots: { default: 'store_basic_info' },
|
||||
},
|
||||
{
|
||||
field: 'online_consultation_config',
|
||||
@@ -40,47 +38,18 @@ export const gridOptions: VxeGridProps<RowType> = {
|
||||
slots: { default: 'online_consultation_config' },
|
||||
},
|
||||
{
|
||||
field: 'is_shipping_free',
|
||||
field: 'store_config_1',
|
||||
align: 'left',
|
||||
title: '是否包邮',
|
||||
slots: { default: 'is_shipping_free' },
|
||||
width: 120,
|
||||
title: '开关配置',
|
||||
width: 330,
|
||||
slots: { default: 'store_config_1' },
|
||||
},
|
||||
{
|
||||
field: 'bank_card_report_status',
|
||||
field: 'store_config_2',
|
||||
align: 'left',
|
||||
title: '银行卡报备',
|
||||
slots: { default: 'bank_card_report_status' },
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
// 订阅价格波动列(显示为开关)
|
||||
field: 'subscribe_price_change',
|
||||
align: 'left',
|
||||
title: '订阅价格',
|
||||
slots: { default: 'subscribe_price_change' },
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
field: 'see_rate',
|
||||
align: 'left',
|
||||
title: '查看毛利率',
|
||||
slots: { default: 'see_rate' },
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
field: 'allow_insurance_category',
|
||||
align: 'left',
|
||||
title: '医保可选',
|
||||
slots: { default: 'allow_insurance_category' },
|
||||
width: 110,
|
||||
},
|
||||
{
|
||||
field: 'salesperson_see_price',
|
||||
align: 'left',
|
||||
title: '推广员可见价格',
|
||||
slots: { default: 'salesperson_see_price' },
|
||||
width: 130,
|
||||
title: '业务配置',
|
||||
width: 330,
|
||||
slots: { default: 'store_config_2' },
|
||||
},
|
||||
{
|
||||
field: 'qr_code',
|
||||
@@ -89,24 +58,8 @@ export const gridOptions: VxeGridProps<RowType> = {
|
||||
slots: { default: 'qr_code' },
|
||||
width: 130,
|
||||
},
|
||||
{
|
||||
field: 'erp_id',
|
||||
align: 'left',
|
||||
title: 'ERP ID',
|
||||
width: 140,
|
||||
slots: { default: 'erp_id_cell' },
|
||||
},
|
||||
// {
|
||||
// field: 'mes_id',
|
||||
// align: 'left',
|
||||
// title: 'MES ID',
|
||||
// width: 140,
|
||||
// slots: { default: 'mes_id_cell' },
|
||||
// },
|
||||
// { field: 'shouzimu', title: '诊所首字母' },
|
||||
{ field: 'position', title: '详细地址', width: 140 },
|
||||
{ field: 'new_admin.nick_name', title: '业务员', slots: { default: 'sales_admin' }, width: 140 },
|
||||
{ field: 'store_admin.phone', title: '管理员手机', slots: { default: 'store_admin_phone' }, width: 140 },
|
||||
{ field: 'new_admin.nick_name', title: '业务员', slots: { default: 'sales_admin' }, width: 160 },
|
||||
{ field: 'mobile', title: '联系人、电话', slots: { default: 'mobile' }, width: 140 },
|
||||
{
|
||||
field: 'start_time',
|
||||
@@ -127,13 +80,11 @@ export const gridOptions: VxeGridProps<RowType> = {
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
// 请求后端接口方法
|
||||
// 注意:会自动添加type=0参数,只查询诊所类型的数据
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getStoreList({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
type: 0, // 固定为诊所类型
|
||||
type: 0,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
@@ -142,19 +93,16 @@ export const gridOptions: VxeGridProps<RowType> = {
|
||||
height: 'auto',
|
||||
border: false,
|
||||
toolbarConfig: {
|
||||
// 是否显示搜索表单控制按钮
|
||||
// @ts-ignore 正式环境时有完整的类型声明
|
||||
search: true,
|
||||
refresh: true, // 刷新
|
||||
print: false, // 打印
|
||||
export: false, // 导出
|
||||
// custom: true, // 自定义列
|
||||
zoom: true, // 最大化最小化
|
||||
refresh: true,
|
||||
print: false,
|
||||
export: false,
|
||||
zoom: true,
|
||||
slots: {
|
||||
buttons: 'toolbar-buttons',
|
||||
},
|
||||
custom: {
|
||||
// 自定义列-图标
|
||||
icon: 'vxe-icon-menu',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -8,11 +8,12 @@ import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { Button, Image, message, Modal, Switch, Tag } from 'ant-design-vue';
|
||||
import { Button, Image, message, Modal, Tag } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { Icon } from '#/components/icon';
|
||||
import QrCodePreview from '#/components/modal/QrCodePreview.vue';
|
||||
import PromoterInfoCard from '#/components/promoter/PromoterInfoCard.vue';
|
||||
import { TableAction } from '#/components/table-action';
|
||||
|
||||
import {
|
||||
@@ -26,8 +27,11 @@ import {
|
||||
updateStoreShippingFree,
|
||||
updateStoreSeeRateStatus,
|
||||
updateStoreSubscribeStatus,
|
||||
updateStoreOrderPricePercentAdjust,
|
||||
} from './api';
|
||||
import FormModalDemo from './components/modal.vue';
|
||||
import StoreBasicInfoCell from './components/cells/StoreBasicInfoCell.vue';
|
||||
import StoreConfigTogglesCell from './components/cells/StoreConfigTogglesCell.vue';
|
||||
import DrugPriceModal from './components/DrugPriceModal.vue';
|
||||
import BindConsultationModal from './components/BindConsultationModal.vue';
|
||||
import StoreExternalFieldModal from './components/StoreExternalFieldModal.vue';
|
||||
@@ -296,6 +300,13 @@ const updateSalespersonSeePrice = (storeId: number) => {
|
||||
});
|
||||
};
|
||||
|
||||
const updateOrderPricePercentAdjust = (id: number) => {
|
||||
updateStoreOrderPricePercentAdjust({ id }).then(() => {
|
||||
message.success('修改成功!');
|
||||
gridApi.query();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量同步总仓库药品
|
||||
*/
|
||||
@@ -408,14 +419,48 @@ const handleSwitchClinicType = (row: any) => {
|
||||
</template>
|
||||
</TableAction>
|
||||
</template>
|
||||
<template #sales_admin="{ row }">
|
||||
<div>
|
||||
<div>{{ row.new_admin?.nick_name || '-' }}</div>
|
||||
<div v-if="row.new_admin?.phone" class="text-xs text-gray-500">{{ row.new_admin.phone }}</div>
|
||||
</div>
|
||||
<template #store_basic_info="{ row }">
|
||||
<StoreBasicInfoCell
|
||||
:row="row"
|
||||
variant="clinic"
|
||||
:on-copy="copyText"
|
||||
:on-edit-external-field="showStoreExternalFieldModal"
|
||||
/>
|
||||
</template>
|
||||
<template #store_admin_phone="{ row }">
|
||||
{{ row.store_admin?.phone || '-' }}
|
||||
<template #store_config_1="{ row }">
|
||||
<StoreConfigTogglesCell
|
||||
:row="row"
|
||||
section="switches"
|
||||
:get-bank-card-report-tag-color="getBankCardReportTagColor"
|
||||
:get-bank-card-report-tag-text="getBankCardReportTagText"
|
||||
:on-shipping-free="updateShippingFree"
|
||||
:on-subscribe="updateSubscribe"
|
||||
:on-see-rate="updateSeeRate"
|
||||
:on-order-price-adjust="updateOrderPricePercentAdjust"
|
||||
:on-bank-card-report="handleBankCardReportColumnClick"
|
||||
/>
|
||||
</template>
|
||||
<template #store_config_2="{ row }">
|
||||
<StoreConfigTogglesCell
|
||||
:row="row"
|
||||
section="business"
|
||||
show-clinic-type
|
||||
show-insurance
|
||||
show-salesperson-see-price
|
||||
:get-bank-card-report-tag-color="getBankCardReportTagColor"
|
||||
:get-bank-card-report-tag-text="getBankCardReportTagText"
|
||||
:on-shipping-free="updateShippingFree"
|
||||
:on-subscribe="updateSubscribe"
|
||||
:on-see-rate="updateSeeRate"
|
||||
:on-order-price-adjust="updateOrderPricePercentAdjust"
|
||||
:on-bank-card-report="handleBankCardReportColumnClick"
|
||||
:on-clinic-type="handleSwitchClinicType"
|
||||
:on-allow-insurance="updateAllowInsurance"
|
||||
:on-salesperson-see-price="updateSalespersonSeePrice"
|
||||
/>
|
||||
</template>
|
||||
<template #sales_admin="{ row }">
|
||||
<PromoterInfoCard :admin="row.new_admin" size="compact" />
|
||||
</template>
|
||||
<template #mobile="{ row }">
|
||||
<div>
|
||||
@@ -444,37 +489,6 @@ const handleSwitchClinicType = (row: any) => {
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #erp_id_cell="{ row }">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
class="h-auto p-0"
|
||||
@click="showStoreExternalFieldModal(row, 'erp_id', 'ERP ID')"
|
||||
>
|
||||
{{ row.erp_id != null && row.erp_id !== '' ? row.erp_id : '-' }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #mes_id_cell="{ row }">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
class="h-auto p-0"
|
||||
@click="showStoreExternalFieldModal(row, 'mes_id', 'MES ID')"
|
||||
>
|
||||
{{ row.mes_id != null && row.mes_id !== '' ? row.mes_id : '-' }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #type="{ row }">
|
||||
</template>
|
||||
<template #clinic_type="{ row }">
|
||||
<Tag
|
||||
:color="row.clinic_type === 1 ? 'blue' : row.clinic_type === 2 ? 'green' : 'default'"
|
||||
class="clinic-type-tag cursor-pointer"
|
||||
@click="handleSwitchClinicType(row)"
|
||||
>
|
||||
{{ row.clinic_type === 1 ? '西医诊所' : row.clinic_type === 2 ? '中医诊所' : '未设置' }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template #online_consultation_config="{ row }">
|
||||
<div
|
||||
v-if="row.is_internet_medical === 1 && row.online_consultation_doctor_name"
|
||||
@@ -495,61 +509,6 @@ const handleSwitchClinicType = (row: any) => {
|
||||
绑定
|
||||
</Button>
|
||||
</template>
|
||||
<template #is_shipping_free="{ row }">
|
||||
<Switch
|
||||
:checked="row.is_shipping_free"
|
||||
:checked-value="1"
|
||||
:un-checked-value="0"
|
||||
checked-children="包邮"
|
||||
un-checked-children="不包邮"
|
||||
@click="updateShippingFree(row.id)"
|
||||
/>
|
||||
</template>
|
||||
<template #bank_card_report_status="{ row }">
|
||||
<Tag
|
||||
:color="getBankCardReportTagColor(row.bank_card_report_status)"
|
||||
style="cursor: pointer"
|
||||
@click="handleBankCardReportColumnClick(row)"
|
||||
>
|
||||
{{ getBankCardReportTagText(row) }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template #subscribe_price_change="{ row }">
|
||||
<Tag
|
||||
:color="row.subscribe_price_change === 0 ? 'success' : 'default'"
|
||||
style="cursor: pointer"
|
||||
@click="updateSubscribe(row.id)"
|
||||
>
|
||||
{{ row.subscribe_price_change === 0 ? '订阅' : '不订阅' }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template #see_rate="{ row }">
|
||||
<Tag
|
||||
:color="Number(row.see_rate) === 1 ? 'success' : 'default'"
|
||||
style="cursor: pointer"
|
||||
@click="updateSeeRate(row.id)"
|
||||
>
|
||||
{{ Number(row.see_rate) === 1 ? '可查看' : '不可查看' }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template #allow_insurance_category="{ row }">
|
||||
<Tag
|
||||
:color="Number(row.allow_insurance_category) === 1 ? 'success' : 'default'"
|
||||
style="cursor: pointer"
|
||||
@click="updateAllowInsurance(row.id)"
|
||||
>
|
||||
{{ Number(row.allow_insurance_category) === 1 ? '可选医保' : '仅自费' }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template #salesperson_see_price="{ row }">
|
||||
<Tag
|
||||
:color="Number(row.salesperson_see_price) === 1 ? 'success' : 'default'"
|
||||
style="cursor: pointer"
|
||||
@click="updateSalespersonSeePrice(row.id)"
|
||||
>
|
||||
{{ Number(row.salesperson_see_price) === 1 ? '可见' : '不可见' }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template #start-time="{ row }">
|
||||
早:<Tag color="success">{{ row.start_time }}</Tag>
|
||||
<br />
|
||||
@@ -676,19 +635,4 @@ const handleSwitchClinicType = (row: any) => {
|
||||
.qr_code:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 诊所类型标签样式 */
|
||||
.clinic-type-tag {
|
||||
transition: all 0.2s ease;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
17
apps/web-antd/src/views/system/system-config/api/index.ts
Normal file
17
apps/web-antd/src/views/system/system-config/api/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
const prefix = 'system-config/';
|
||||
|
||||
export async function getSystemConfigList() {
|
||||
return requestClient.get<any>(`${prefix}list`);
|
||||
}
|
||||
|
||||
export async function saveSystemConfig(items: Array<{ config_key: string; config_value: string }>) {
|
||||
return requestClient.post<any>(`${prefix}save`, { items });
|
||||
}
|
||||
|
||||
export async function getPriceAdjustConfig(storeId?: number) {
|
||||
return requestClient.get<any>(`${prefix}price-adjust-config`, {
|
||||
params: storeId ? { store_id: storeId } : {},
|
||||
});
|
||||
}
|
||||
139
apps/web-antd/src/views/system/system-config/index.vue
Normal file
139
apps/web-antd/src/views/system/system-config/index.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
|
||||
|
||||
import { Button, Card, Input, InputNumber, message, Radio, Space, Tag } from 'ant-design-vue';
|
||||
|
||||
|
||||
|
||||
import type { QuickDiscountOption } from '#/utils/pricePercentAdjust';
|
||||
|
||||
|
||||
|
||||
import { getSystemConfigList, saveSystemConfig } from './api';
|
||||
|
||||
|
||||
|
||||
defineOptions({ name: 'SystemConfig' });
|
||||
|
||||
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const saving = ref(false);
|
||||
|
||||
const scope = ref<'both' | 'sale_only'>('sale_only');
|
||||
|
||||
const quickOptions = ref<QuickDiscountOption[]>([
|
||||
|
||||
{ name: '九五折', value: 95 },
|
||||
|
||||
{ name: '九折', value: 90 },
|
||||
|
||||
{ name: '八五折', value: 85 },
|
||||
|
||||
{ name: '八折', value: 80 },
|
||||
|
||||
{ name: '涨10%', value: 110 },
|
||||
|
||||
{ name: '涨20%', value: 120 },
|
||||
|
||||
]);
|
||||
|
||||
const newQuickName = ref('');
|
||||
|
||||
const newQuickValue = ref<number | null>(null);
|
||||
|
||||
|
||||
|
||||
function parseQuickOptions(raw: unknown): QuickDiscountOption[] {
|
||||
|
||||
if (typeof raw === 'string') {
|
||||
|
||||
try {
|
||||
|
||||
const parsed = JSON.parse(raw);
|
||||
|
||||
return parseQuickOptions(parsed);
|
||||
|
||||
} catch {
|
||||
|
||||
return quickOptions.value;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!Array.isArray(raw)) return quickOptions.value;
|
||||
|
||||
const list: QuickDiscountOption[] = [];
|
||||
|
||||
for (const item of raw) {
|
||||
|
||||
if (item && typeof item === 'object' && 'value' in item) {
|
||||
|
||||
const val = Number((item as QuickDiscountOption).value);
|
||||
|
||||
if (val > 0) list.push({ name: String((item as QuickDiscountOption).name || `${val}%`), value: val });
|
||||
|
||||
} else if (typeof item === 'number' && item > 0) {
|
||||
|
||||
list.push({ name: `${item}%`, value: item });
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return list.length ? list : quickOptions.value;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function load() {
|
||||
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
|
||||
const list = await getSystemConfigList();
|
||||
|
||||
const rows = Array.isArray(list) ? list : list?.data || [];
|
||||
|
||||
for (const row of rows) {
|
||||
|
||||
if (row.config_key === 'order_price_adjust_scope') {
|
||||
|
||||
scope.value = row.config_value === 'both' ? 'both' : 'sale_only';
|
||||
|
||||
}
|
||||
|
||||
if (row.config_key === 'order_discount_quick_options') {
|
||||
|
||||
quickOptions.value = parseQuickOptions(row.config_value);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} finally {
|
||||
|
||||
loading.value = false;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
function addQuick() {
|
||||
|
||||
const v = Number(newQuickValue.value);
|
||||
|
||||
const name = String(newQuickName.value || '').trim();
|
||||
Reference in New Issue
Block a user