diff --git a/src/pages/menuPages/sms/components/SmsEditorFormBody.vue b/src/pages/menuPages/sms/components/SmsEditorFormBody.vue index da9de5a..0a2ea0c 100644 --- a/src/pages/menuPages/sms/components/SmsEditorFormBody.vue +++ b/src/pages/menuPages/sms/components/SmsEditorFormBody.vue @@ -1,7 +1,7 @@ @@ -79,6 +104,10 @@ import type { FunctionItem } from '@/model/Item' import type { Customer } from '@/model/customer' import type { Sms } from '@/model/sms' import { getCustomerList } from '@/api/sms' +import { getConfiguration } from '@/api/configuration' +import { nextTick } from 'vue' + +const NAME_TOKEN = '%نام' const props = defineProps<{ form: Sms @@ -87,10 +116,14 @@ const props = defineProps<{ }>() const formRef = ref() +const messageAreaRef = ref<{ $el?: HTMLElement } | null>(null) const selectedGroup = ref([]) const groupNumbers = ref([]) const customersLoading = ref(false) +const appName = ref('') let baseGroupNumbers: Customer[] = [] +let cursorStart = 0 +let cursorEnd = 0 const columns = [ { title: 'کد کاربر', dataIndex: 'id' }, @@ -98,16 +131,91 @@ const columns = [ { title: 'شماره همراه', dataIndex: 'mobile' } ] +const appDisplayName = computed(() => appName.value.trim() || 'اپ') + +const messageRules = [ + { + required: true, + validator: async (_: unknown, value: string) => { + if (!String(value ?? '').trim()) { + return Promise.reject('متن پیام خالی است') + } + return Promise.resolve() + } + } +] + +const rowSelection = computed(() => ({ + selectedRowKeys: props.form.numbers ?? [], + onChange: onSelectChange, + preserveSelectedRowKeys: true +})) + +onMounted(() => { + void loadAppName() +}) + +async function loadAppName() { + try { + const rows = await getConfiguration() + const list = Array.isArray(rows) ? rows : [] + appName.value = String(list.find((r) => r.key === 'name_fa')?.value ?? '').trim() + } catch { + appName.value = '' + } +} + +function getTextareaEl(): HTMLTextAreaElement | null { + const root = messageAreaRef.value?.$el + if (!root) return null + if (root instanceof HTMLTextAreaElement) return root + return root.querySelector?.('textarea') ?? null +} + +function rememberCursor() { + const ta = getTextareaEl() + if (!ta) return + cursorStart = ta.selectionStart ?? 0 + cursorEnd = ta.selectionEnd ?? cursorStart +} + function addName() { - props.form.message = (props.form.message ?? '').concat('', '%name') + rememberCursor() + const text = props.form.message ?? '' + const start = cursorStart + const end = cursorEnd + props.form.message = text.slice(0, start) + NAME_TOKEN + text.slice(end) + const nextPos = start + NAME_TOKEN.length + cursorStart = nextPos + cursorEnd = nextPos + nextTick(() => { + const ta = getTextareaEl() + if (!ta) return + ta.focus() + ta.setSelectionRange(nextPos, nextPos) + }) +} + +/** نام اپ را به‌صورت خط آخر اجباری می‌چسباند */ +function buildFinalMessage(): string { + const body = String(props.form.message ?? '').trim() + const app = appDisplayName.value + if (!body) return app + const lines = body.split(/\r?\n/) + const last = (lines[lines.length - 1] ?? '').trim() + if (last === app) return body + return `${body}\n${app}` } function filterNumbers(val: string) { - if (val !== '') { - groupNumbers.value = baseGroupNumbers.filter((v) => v.name?.includes(val)) - } else { + const q = String(val ?? '').trim() + if (!q) { groupNumbers.value = baseGroupNumbers + return } + groupNumbers.value = baseGroupNumbers.filter( + (v) => v.name?.includes(q) || String(v.mobile ?? '').includes(q) + ) } async function loadGroupNumbers(val: string[]) { @@ -123,7 +231,6 @@ async function loadGroupNumbers(val: string[]) { const { customers } = await getCustomerList(val) groupNumbers.value = customers baseGroupNumbers = customers - // فقط شماره‌های همین لیست مشتری بمانند const allowed = new Set(customers.map((c) => c.mobile).filter(Boolean) as string[]) props.form.numbers = (props.form.numbers ?? []).filter((n) => allowed.has(n)) } catch (e) { @@ -133,9 +240,22 @@ async function loadGroupNumbers(val: string[]) { } } -function onSelectChange(changableRowKeys: string[]) { - const allowed = new Set(baseGroupNumbers.map((c) => c.mobile).filter(Boolean) as string[]) - props.form.numbers = changableRowKeys.filter((n) => allowed.has(n)) +/** + * تیک‌های خارج از نتیجهٔ سرچ فعلی حفظ می‌شوند؛ + * فقط وضعیت ردیف‌های قابل‌مشاهده از selectedRowKeys به‌روز می‌شود. + */ +function onSelectChange(selectedRowKeys: string[]) { + const visible = new Set( + groupNumbers.value.map((c) => c.mobile).filter(Boolean) as string[] + ) + const selectedVisible = new Set(selectedRowKeys) + const next = new Set((props.form.numbers ?? []).filter((m) => !visible.has(m))) + for (const mobile of selectedVisible) { + if (baseGroupNumbers.some((c) => c.mobile === mobile)) { + next.add(mobile) + } + } + props.form.numbers = Array.from(next) } function isSelected(mobile?: string) { @@ -162,10 +282,26 @@ function resetAll() { groupNumbers.value = [] baseGroupNumbers = [] props.form.numbers = [] + props.form.message = '' + cursorStart = 0 + cursorEnd = 0 } defineExpose({ validateFields: () => formRef.value?.validateFields(), - resetAll + resetAll, + buildFinalMessage }) + + diff --git a/src/pages/menuPages/sms/index.vue b/src/pages/menuPages/sms/index.vue index 662609e..0cff6a2 100644 --- a/src/pages/menuPages/sms/index.vue +++ b/src/pages/menuPages/sms/index.vue @@ -53,6 +53,7 @@ const table = ref void }>(null) const smsEditorRef = ref<{ validateFields?: () => ReturnType> resetAll?: () => void + buildFinalMessage?: () => string }>() const sendStore = useGroupSmsSendStore() @@ -138,7 +139,7 @@ function ok() { return } - const text = form.value.message + const text = smsEditorRef.value?.buildFinalMessage?.() ?? form.value.message addSMSVisible.value = false resetData() await sendStore.start(numbers, text)