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

This commit is contained in:
李琦
2026-06-18 15:05:02 +08:00
parent d9db629c5d
commit 073f7f882b
10 changed files with 750 additions and 309 deletions

View File

@@ -164,6 +164,19 @@ export const modalFormProps: VbenFormProps = {
label: '简介',
rules: 'required',
},
{
component: 'InputNumber',
componentProps: {
min: 0,
precision: 2,
placeholder: '请输入挂号费',
class: 'w-full',
},
fieldName: 'register_price',
formItemClass: 'col-span-6',
label: '挂号费',
defaultValue: 0,
},
{
component: 'Avatar',
componentProps: {

View File

@@ -0,0 +1,169 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { Page } from '@vben/common-ui';
import { Button, Card, Empty, Modal, Pagination, Spin, message } from 'ant-design-vue';
import type { FileGalleryItem } from '#/api/core/file-gallery';
import {
deleteFileGalleryItem,
getFileGalleryList,
syncFileGalleryFromOss,
} from '#/api/core/file-gallery';
defineOptions({ name: 'FileGallery' });
const loading = ref(false);
const syncing = ref(false);
const items = ref<FileGalleryItem[]>([]);
const total = ref(0);
const page = ref(1);
const pageSize = ref(24);
async function load() {
loading.value = true;
try {
const res = await getFileGalleryList({
page: page.value,
page_size: pageSize.value,
pid: 0,
type: 0,
});
const data = (res as any)?.data ?? res;
items.value = data?.items ?? [];
total.value = data?.total ?? 0;
} finally {
loading.value = false;
}
}
async function handleSync() {
syncing.value = true;
try {
const res = await syncFileGalleryFromOss();
const data = (res as any)?.data ?? res;
message.success(`同步完成:扫描 ${data?.scanned ?? 0} 个,新增 ${data?.added ?? 0} 张,跳过 ${data?.skipped ?? 0} 张`);
await load();
} finally {
syncing.value = false;
}
}
function handleDelete(item: FileGalleryItem) {
Modal.confirm({
title: '确认删除',
content: '删除后将同时移除 OSS 中的文件,此操作不可恢复,是否继续?',
okText: '删除',
okType: 'danger',
cancelText: '取消',
async onOk() {
await deleteFileGalleryItem(item.id);
message.success('删除成功');
await load();
},
});
}
function onPageChange(p: number) {
page.value = p;
load();
}
load();
</script>
<template>
<Page auto-content-height title="图库管理">
<Card>
<div class="toolbar">
<Button type="primary" :loading="syncing" @click="handleSync">同步 OSS 图片</Button>
<Button :loading="loading" @click="load">刷新</Button>
</div>
<Spin :spinning="loading">
<div v-if="items.length" class="gallery-grid">
<div v-for="item in items" :key="item.id" class="gallery-card">
<img :src="item.url" alt="" class="gallery-thumb" />
<div class="gallery-meta">
<div>{{ item.file_size_text || '-' }}</div>
<div>{{ item.created_at || '-' }}</div>
</div>
<Button danger size="small" block @click="handleDelete(item)">删除</Button>
</div>
</div>
<Empty v-else description="暂无图片,可点击「同步 OSS 图片」导入" />
</Spin>
<div v-if="total > pageSize" class="gallery-pagination">
<Pagination
:current="page"
:page-size="pageSize"
:total="total"
show-size-changer
@change="onPageChange"
/>
</div>
</Card>
</Page>
</template>
<style scoped lang="scss">
@use '#/components/form/components/picker-card-theme.scss' as theme;
.toolbar {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 16px;
}
.gallery-card {
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px;
border: 1px solid #e5e6eb;
border-radius: 8px;
background: #fff;
}
.gallery-thumb {
width: 100%;
height: 120px;
object-fit: cover;
border-radius: 6px;
background: #f5f5f5;
}
.gallery-meta {
font-size: 12px;
color: #86909c;
text-align: center;
}
.gallery-pagination {
margin-top: 20px;
display: flex;
justify-content: center;
}
.dark {
.gallery-card {
@include theme.picker-card-dark-props;
}
.gallery-thumb {
background: #374151;
}
.gallery-meta {
@include theme.picker-text-secondary-dark;
}
}
</style>

View File

@@ -1,139 +1,155 @@
<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();
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { Button, Card, Input, InputNumber, message, Radio, Space, Switch, 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);
const inputAutoAuditPass = ref(true);
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);
}
if (row.config_key === 'input_auto_audit_pass') {
inputAutoAuditPass.value = row.config_value === '1' || row.config_value === true || row.config_value === 'true';
}
}
} finally {
loading.value = false;
}
}
function addQuick() {
const v = Number(newQuickValue.value);
const name = String(newQuickName.value || '').trim();
if (!name || !v || v <= 0) {
message.warning('请填写名称和有效比例值');
return;
}
if (!quickOptions.value.some((o) => o.value === v && o.name === name)) {
quickOptions.value = [...quickOptions.value, { name, value: v }].sort((a, b) => b.value - a.value);
}
newQuickName.value = '';
newQuickValue.value = null;
}
function removeQuick(opt: QuickDiscountOption) {
quickOptions.value = quickOptions.value.filter((o) => !(o.name === opt.name && o.value === opt.value));
}
async function handleSave() {
saving.value = true;
try {
await saveSystemConfig([
{ config_key: 'order_price_adjust_scope', config_value: scope.value },
{
config_key: 'order_discount_quick_options',
config_value: JSON.stringify(quickOptions.value),
},
{
config_key: 'input_auto_audit_pass',
config_value: inputAutoAuditPass.value ? '1' : '0',
},
]);
message.success('保存成功');
} finally {
saving.value = false;
}
}
onMounted(load);
</script>
<template>
<Page auto-content-height title="系统配置">
<Card :loading="loading" title="订单调价配置">
<div class="mb-6">
<div class="mb-2 font-medium">订单打折时价格浮动范围</div>
<Radio.Group v-model:value="scope">
<Radio value="sale_only">仅售价浮动</Radio>
<Radio value="both">供货价与售价一起浮动</Radio>
</Radio.Group>
</div>
<div class="mb-6">
<div class="mb-2 font-medium">调价快捷选项(name 展示名,value 比例:100=原价,90=九折,120=涨20%)</div>
<Space wrap class="mb-3">
<Tag
v-for="item in quickOptions"
:key="item.name + item.value"
closable
:color="item.value < 100 ? 'processing' : 'warning'"
@close="removeQuick(item)"
>
{{ item.name }}({{ item.value }}%)
</Tag>
</Space>
<Space wrap>
<Input v-model:value="newQuickName" placeholder="名称,如九折" style="width: 140px" />
<InputNumber v-model:value="newQuickValue" :min="1" :max="500" placeholder="比例值" />
<Button @click="addQuick">添加快捷项</Button>
</Space>
</div>
<Button type="primary" :loading="saving" @click="handleSave">保存配置</Button>
</Card>
<Card :loading="loading" class="mt-4" title="录入审核配置">
<div class="mb-2 font-medium">诊所/医生信息预填录入后是否自动通过审核</div>
<Switch v-model:checked="inputAutoAuditPass" checked-children="是" un-checked-children="否" />
<div class="mt-4">
<Button type="primary" :loading="saving" @click="handleSave">保存配置</Button>
</div>
</Card>
</Page>
</template>