Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2040c2b15 | ||
|
|
0d18089f71 |
@@ -3,6 +3,7 @@
|
||||
* 带快捷预设的搜索时间范围选择器
|
||||
* - 上方 RangePicker 手选,下方快捷 link:今天 / 本周 / 本月 / 上一个月 / 下一个月
|
||||
* - 支持 valueFormat='YYYY-MM-DD'(VbenForm)或 Dayjs 二元组(对账页)
|
||||
* - 界面按天展示;写出值时开始为当天 00:00:00、结束为当天 23:59:59,避免漏查当天末笔
|
||||
*/
|
||||
import type { Dayjs } from 'dayjs';
|
||||
|
||||
@@ -69,8 +70,9 @@ const innerRange = computed<[Dayjs, Dayjs] | null>({
|
||||
emit('change', null);
|
||||
return;
|
||||
}
|
||||
// formatRangeByValueFormat 内会规范到 00:00:00 / 23:59:59
|
||||
const formatted = formatRangeByValueFormat(
|
||||
[next[0].startOf('day'), next[1].startOf('day')],
|
||||
[next[0], next[1]],
|
||||
props.valueFormat,
|
||||
);
|
||||
mValue.value = formatted as [string, string] | [Dayjs, Dayjs];
|
||||
@@ -145,7 +147,6 @@ const pickerValue = computed<[Dayjs, Dayjs] | undefined>({
|
||||
v-bind="$attrs"
|
||||
:disabled="disabled"
|
||||
:format="format"
|
||||
:value-format="valueFormat"
|
||||
class="search-time-range-picker__picker w-full"
|
||||
@change="onPickerChange"
|
||||
/>
|
||||
|
||||
@@ -9,9 +9,21 @@ export type SearchTimePreset =
|
||||
| 'prevMonth'
|
||||
| 'nextMonth';
|
||||
|
||||
/** 界面展示用纯日期;提交给后端时再拼时分秒 */
|
||||
const YMD = 'YYYY-MM-DD';
|
||||
/** 查询边界:开始日 00:00:00、结束日 23:59:59,避免当天末笔被滤掉 */
|
||||
const YMD_START = 'YYYY-MM-DD 00:00:00';
|
||||
const YMD_END = 'YYYY-MM-DD 23:59:59';
|
||||
|
||||
/** 今天日期(Dayjs) */
|
||||
/**
|
||||
* 将区间规范为「开始日 00:00:00 ~ 结束日 23:59:59」
|
||||
* 搜索按天选时,结束若不落到当天最后一秒,当天 0 点之后的数据会被漏掉
|
||||
*/
|
||||
export function toSearchDayBounds(range: [Dayjs, Dayjs]): [Dayjs, Dayjs] {
|
||||
return [range[0].startOf('day'), range[1].endOf('day')];
|
||||
}
|
||||
|
||||
/** 今天日期(Dayjs,当天 00:00:00) */
|
||||
export function todayDayjs(): Dayjs {
|
||||
return dayjs().startOf('day');
|
||||
}
|
||||
@@ -24,30 +36,27 @@ export function startOfWeekMonday(base: Dayjs = todayDayjs()): Dayjs {
|
||||
return d.subtract(diff, 'day');
|
||||
}
|
||||
|
||||
/** 本月1日 ~ 今天(字符串,供 RangePicker valueFormat) */
|
||||
/** 本月1日 00:00:00 ~ 今天 23:59:59(字符串,供表单默认值 / 接口) */
|
||||
export function monthToTodayRangeString(): [string, string] {
|
||||
const today = todayDayjs();
|
||||
return [today.startOf('month').format(YMD), today.format(YMD)];
|
||||
return rangeDayjsToString(monthToTodayRangeDayjs());
|
||||
}
|
||||
|
||||
/** 本月1日 ~ 今天(Dayjs) */
|
||||
/** 本月1日 ~ 今天(Dayjs,已含起止边界) */
|
||||
export function monthToTodayRangeDayjs(): [Dayjs, Dayjs] {
|
||||
const today = todayDayjs();
|
||||
return [today.startOf('month'), today];
|
||||
return toSearchDayBounds([today.startOf('month'), today]);
|
||||
}
|
||||
|
||||
/** 近 N 天(含今天),默认 7 天 */
|
||||
/** 近 N 天(含今天),默认 7 天;字符串已含 00:00:00 / 23:59:59 */
|
||||
export function lastNDaysRangeString(days = 7): [string, string] {
|
||||
const today = todayDayjs();
|
||||
const start = today.subtract(Math.max(days - 1, 0), 'day');
|
||||
return [start.format(YMD), today.format(YMD)];
|
||||
return rangeDayjsToString(lastNDaysRangeDayjs(days));
|
||||
}
|
||||
|
||||
/** 近 N 天(Dayjs) */
|
||||
/** 近 N 天(Dayjs,已含起止边界) */
|
||||
export function lastNDaysRangeDayjs(days = 7): [Dayjs, Dayjs] {
|
||||
const today = todayDayjs();
|
||||
const start = today.subtract(Math.max(days - 1, 0), 'day');
|
||||
return [start, today];
|
||||
return toSearchDayBounds([start, today]);
|
||||
}
|
||||
|
||||
/** 将 Dayjs / 字符串 / 时间戳统一为 YYYY-MM-DD */
|
||||
@@ -98,17 +107,17 @@ export function getMonthAnchorDayjs(current: unknown): Dayjs {
|
||||
return todayDayjs();
|
||||
}
|
||||
|
||||
/** 自然月整月,结束日不超过今天 */
|
||||
/** 自然月整月,结束日不超过今天(返回值已含起止边界) */
|
||||
function fullMonthRangeCapped(
|
||||
month: Dayjs,
|
||||
): [Dayjs, Dayjs] {
|
||||
const start = month.startOf('month');
|
||||
const end = month.endOf('month').startOf('day');
|
||||
const today = todayDayjs();
|
||||
return [start, end.isAfter(today) ? today : end];
|
||||
return toSearchDayBounds([start, end.isAfter(today) ? today : end]);
|
||||
}
|
||||
|
||||
/** 按预设计算区间(Dayjs) */
|
||||
/** 按预设计算区间(Dayjs,已含 00:00:00 / 23:59:59 边界) */
|
||||
export function resolveSearchTimePreset(
|
||||
preset: SearchTimePreset,
|
||||
current: unknown,
|
||||
@@ -116,11 +125,11 @@ export function resolveSearchTimePreset(
|
||||
const today = todayDayjs();
|
||||
switch (preset) {
|
||||
case 'today':
|
||||
return [today, today];
|
||||
return toSearchDayBounds([today, today]);
|
||||
case 'thisWeek':
|
||||
return [startOfWeekMonday(today), today];
|
||||
return toSearchDayBounds([startOfWeekMonday(today), today]);
|
||||
case 'thisMonth':
|
||||
return [today.startOf('month'), today];
|
||||
return toSearchDayBounds([today.startOf('month'), today]);
|
||||
case 'prevMonth': {
|
||||
const anchor = getMonthAnchorDayjs(current);
|
||||
return fullMonthRangeCapped(anchor.subtract(1, 'month'));
|
||||
@@ -134,36 +143,44 @@ export function resolveSearchTimePreset(
|
||||
}
|
||||
}
|
||||
|
||||
/** 按预设计算区间(字符串) */
|
||||
/** 按预设计算区间(字符串,已含时分秒) */
|
||||
export function resolveSearchTimePresetString(
|
||||
preset: SearchTimePreset,
|
||||
current: unknown,
|
||||
): [string, string] {
|
||||
const [a, b] = resolveSearchTimePreset(preset, current);
|
||||
return [a.format(YMD), b.format(YMD)];
|
||||
return rangeDayjsToString(resolveSearchTimePreset(preset, current));
|
||||
}
|
||||
|
||||
/** Dayjs 区间转字符串 */
|
||||
/**
|
||||
* Dayjs 区间转接口字符串
|
||||
* 固定开始 00:00:00、结束 23:59:59,与按天筛选的业务口径一致
|
||||
*/
|
||||
export function rangeDayjsToString(
|
||||
range: [Dayjs, Dayjs],
|
||||
): [string, string] {
|
||||
return [range[0].format(YMD), range[1].format(YMD)];
|
||||
const [start, end] = toSearchDayBounds(range);
|
||||
return [start.format(YMD_START), end.format(YMD_END)];
|
||||
}
|
||||
|
||||
/** 字符串区间转 Dayjs */
|
||||
/** 字符串区间转 Dayjs(兼容纯日期或带时分秒) */
|
||||
export function rangeStringToDayjs(
|
||||
range: [string, string],
|
||||
): [Dayjs, Dayjs] {
|
||||
return [dayjs(range[0], YMD), dayjs(range[1], YMD)];
|
||||
return toSearchDayBounds([dayjs(range[0]), dayjs(range[1])]);
|
||||
}
|
||||
|
||||
/** 将区间按 valueFormat 输出 */
|
||||
/**
|
||||
* 将区间按表单模式输出
|
||||
* valueFormat=YYYY-MM-DD 表示「按天选择」,值仍带完整时分秒边界供接口使用
|
||||
* 未传 valueFormat 时返回 Dayjs(同样规范到起止边界)
|
||||
*/
|
||||
export function formatRangeByValueFormat(
|
||||
range: [Dayjs, Dayjs],
|
||||
valueFormat?: string,
|
||||
): [string, string] | [Dayjs, Dayjs] {
|
||||
const bounded = toSearchDayBounds(range);
|
||||
if (valueFormat === YMD) {
|
||||
return rangeDayjsToString(range);
|
||||
return rangeDayjsToString(bounded);
|
||||
}
|
||||
return range;
|
||||
return bounded;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,20 @@ export async function getSystemConfigByKeys(keys: string[]) {
|
||||
});
|
||||
}
|
||||
|
||||
/** 满额包邮门槛(老库 yii_system_config config_type=2 type=2),返回 { id, name, rule, value } */
|
||||
export async function getFreeShippingConfig() {
|
||||
return requestClient.get<any>(`${prefix}free-shipping-config`);
|
||||
}
|
||||
|
||||
/** 保存满额包邮门槛,只传 value 时后端保留 name/rule 原值 */
|
||||
export async function saveFreeShippingConfig(data: {
|
||||
name?: string;
|
||||
rule?: string;
|
||||
value: number;
|
||||
}) {
|
||||
return requestClient.post<any>(`${prefix}save-free-shipping-config`, data);
|
||||
}
|
||||
|
||||
const wxEntryPrefix = 'wx-workbench-entry/';
|
||||
|
||||
/** 某角色已分配的医生端功能入口(含 is_default_fav) */
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 系统配置 - 满额包邮:订单商品金额达到门槛免运费
|
||||
* 数据存老库 yii_system_config(config_type=2, type=2),下单运费计算直接读该行,
|
||||
* 走独立接口 free-shipping-config,不与新库 xk_system_config 的 key-value 混存
|
||||
*/
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { Button, InputNumber, message } from 'ant-design-vue';
|
||||
|
||||
import { getFreeShippingConfig, saveFreeShippingConfig } from '../api';
|
||||
|
||||
defineOptions({ name: 'FreeShippingConfigPanel' });
|
||||
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
/** 门槛金额(元),null 表示尚未加载成功 */
|
||||
const thresholdValue = ref<null | number>(null);
|
||||
/** 配置行是否存在(不存在时禁止保存,提示先初始化 SQL) */
|
||||
const configExists = ref(true);
|
||||
|
||||
/** 加载当前门槛金额,配置行缺失时后端会报错,这里标记后禁用保存 */
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await getFreeShippingConfig();
|
||||
thresholdValue.value = Number(data?.value ?? 0);
|
||||
configExists.value = true;
|
||||
} catch {
|
||||
// 老库无配置行(新环境未跑初始化 SQL)时走到这里
|
||||
configExists.value = false;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 保存门槛金额,name/rule 不传由后端保留库中原值 */
|
||||
async function handleSave() {
|
||||
const v = thresholdValue.value;
|
||||
if (v === null || Number.isNaN(v) || v < 0) {
|
||||
message.warning('请填写有效的包邮门槛金额');
|
||||
return;
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
await saveFreeShippingConfig({ value: v });
|
||||
message.success('保存成功');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="cfg-panel" :class="{ 'opacity-60': loading }">
|
||||
<div class="cfg-panel-body">
|
||||
<div class="mb-2 font-medium text-[hsl(var(--foreground))]">满额包邮门槛(元)</div>
|
||||
<div class="mb-3 text-sm text-[hsl(var(--muted-foreground))]">
|
||||
订单商品金额达到该门槛时免运费;未达到时按收货省份运费(「地区管理」中的快递费)计费。
|
||||
单品包邮、门店包邮、特色方包邮的优先级高于本规则;门槛为 0 表示任意金额均包邮。
|
||||
</div>
|
||||
<InputNumber
|
||||
v-model:value="thresholdValue"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="1"
|
||||
:disabled="!configExists"
|
||||
style="width: 220px"
|
||||
placeholder="请输入门槛金额"
|
||||
>
|
||||
<template #addonAfter> 元 </template>
|
||||
</InputNumber>
|
||||
<div v-if="!configExists" class="mt-3 text-sm text-[hsl(var(--warning))]">
|
||||
未找到包邮配置数据,请先在老库执行初始化 SQL(sql/20260827/01_yii_system_config_free_shipping_init.sql)后刷新
|
||||
</div>
|
||||
</div>
|
||||
<div class="cfg-panel-footer">
|
||||
<Button type="primary" :loading="saving" :disabled="!configExists" @click="handleSave">
|
||||
保存本模块配置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -17,6 +17,7 @@ import AiModelConfigPanel from './components/ai-model-config-panel.vue';
|
||||
import DictSearchConfigPanel from './components/dict-search-config-panel.vue';
|
||||
import DispenseSignConfigPanel from './components/dispense-sign-config-panel.vue';
|
||||
import DoctorLoginConfigPanel from './components/doctor-login-config-panel.vue';
|
||||
import FreeShippingConfigPanel from './components/free-shipping-config-panel.vue';
|
||||
import HomeDisplayConfigPanel from './components/home-display-config-panel.vue';
|
||||
import InputAuditConfigPanel from './components/input-audit-config-panel.vue';
|
||||
import InvoiceNoticeConfigPanel from './components/invoice-notice-config-panel.vue';
|
||||
@@ -83,6 +84,11 @@ const menuItems: MenuItem[] = [
|
||||
component: PriceAdjustConfigPanel,
|
||||
},
|
||||
{ key: 'logistics', title: '物流展示', component: LogisticsConfigPanel },
|
||||
{
|
||||
key: 'free_shipping',
|
||||
title: '满额包邮',
|
||||
component: FreeShippingConfigPanel,
|
||||
},
|
||||
{
|
||||
key: 'public_account_pay',
|
||||
title: '公账支付',
|
||||
|
||||
Reference in New Issue
Block a user