diff --git a/apps/src/views/_shared/admin-config-table.vue b/apps/src/views/_shared/admin-config-table.vue new file mode 100644 index 0000000..cc78c22 --- /dev/null +++ b/apps/src/views/_shared/admin-config-table.vue @@ -0,0 +1,268 @@ + + + + + + + + + {{ slotProps.column.title }} + + + + + {{ stringifyCellValue(getColumnValue(slotProps.record, slotProps.column)) }} + + + + + + + emit('pageChange', page, size)" + @show-size-change="(page: number, size: number) => emit('pageChange', page, size)" + /> + + + + + diff --git a/apps/src/views/_shared/resource-code.ts b/apps/src/views/_shared/resource-code.ts new file mode 100644 index 0000000..06c6440 --- /dev/null +++ b/apps/src/views/_shared/resource-code.ts @@ -0,0 +1,104 @@ +const RESOURCE_NAME_TRANSLATION_ENTRIES = ([ + ['头像框', 'avatar frame'], + ['座驾', 'ride'], + ['坐骑', 'mount'], + ['勋章', 'badge'], + ['徽章', 'badge'], + ['管理员', 'admin'], + ['圣诞', 'christmas'], + ['新年', 'new year'], + ['情人节', 'valentine'], + ['万圣节', 'halloween'], + ['锦鲤', 'koi'], + ['熊猫', 'panda'], + ['独角兽', 'unicorn'], + ['天使', 'angel'], + ['恶魔', 'devil'], + ['皇冠', 'crown'], + ['国王', 'king'], + ['女王', 'queen'], + ['玫瑰', 'rose'], + ['星光', 'starlight'], + ['星星', 'star'], + ['月亮', 'moon'], + ['太阳', 'sun'], + ['彩虹', 'rainbow'], + ['火焰', 'flame'], + ['冰雪', 'snow'], + ['蓝色', 'blue'], + ['红色', 'red'], + ['紫色', 'purple'], + ['粉色', 'pink'], + ['金色', 'gold'], + ['银色', 'silver'], + ['黑色', 'black'], + ['白色', 'white'], + ['绿色', 'green'], + ['黄色', 'yellow'], + ['钻石', 'diamond'], + ['水晶', 'crystal'], + ['幸运', 'lucky'], + ['浪漫', 'romantic'], + ['荣耀', 'glory'], + ['守护', 'guard'], + ['贵族', 'noble'], + ['梦幻', 'dream'], + ['闪耀', 'shining'], + ['限定', 'limited'], + ['豪华', 'deluxe'], + ['高级', 'premium'], + ['经典', 'classic'], + ['热气球', 'hot air balloon'], + ['跑车', 'sports car'], + ['飞船', 'spaceship'], + ['游艇', 'yacht'], + ['摩托', 'motorcycle'], + ['龙', 'dragon'], + ['凤', 'phoenix'], + ['虎', 'tiger'], + ['兔', 'rabbit'], + ['猫', 'cat'], + ['狗', 'dog'], + ['鱼', 'fish'], +] as Array<[string, string]>).toSorted( + (current, next) => next[0].length - current[0].length, +); + +export function normalizeResourceBaseName(value: string) { + const name = value + .replace(/\.[^.]+$/, '') + .replace(/([_-]?(cover|covers|source|resource|resources|ios|android|expand))$/i, '') + .replace(/([_-]?(封面|资源))$/i, '') + .trim(); + return name || value.replace(/\.[^.]+$/, '') || 'resource'; +} + +export function normalizeResourceCode(value: string) { + const baseName = normalizeResourceBaseName(value); + const translated = translateResourceName(baseName); + return slugifyResourceCode(translated) || slugifyResourceCode(baseName) || 'resource'; +} + +function translateResourceName(value: string) { + let result = value.trim(); + RESOURCE_NAME_TRANSLATION_ENTRIES.forEach(([source, target]) => { + result = result.replaceAll(source, ` ${target} `); + }); + return [...result] + .map((char) => { + if (/[\u4E00-\u9FFF]/.test(char)) { + return ` u${char.codePointAt(0)?.toString(16) || ''} `; + } + return char; + }) + .join(''); +} + +function slugifyResourceCode(value: string) { + return value + .normalize('NFKC') + .replaceAll(/[^\d A-Za-z]+/g, ' ') + .trim() + .replaceAll(/\s+/g, '_') + .toLowerCase(); +} diff --git a/apps/src/views/operate/components/gift-batch-drawer.vue b/apps/src/views/operate/components/gift-batch-drawer.vue new file mode 100644 index 0000000..c8bdc39 --- /dev/null +++ b/apps/src/views/operate/components/gift-batch-drawer.vue @@ -0,0 +1,1239 @@ + + + + + + + + 平台 + + + + 礼物类型 + + + + 区域 + + + + 有效期 + + + 收费类型:金币 + + + + + + + + + + {{ showOnlyErrors ? '查看全部' : '只看错误' }} + + + 删除选中 + + + + 已选 {{ selectedRowKeys.length }} 项 + + + + + + + + + + {{ record.coverUploading ? '上传中' : '缺少封面' }} + + {{ record.coverFileName || '-' }} + + + + + + {{ record.sourceUploading ? '上传中' : record.sourceUrl ? '已上传' : '未上传' }} + + {{ record.sourceFileName || '-' }} + + + + + + + + + + + + + + + + + + + + {{ getStatusText(record.status) }} + + + + + 删除 + + + + + + + + + + + + + + + + + diff --git a/apps/src/views/operate/gift-config.vue b/apps/src/views/operate/gift-config.vue index 3c71236..51c9697 100644 --- a/apps/src/views/operate/gift-config.vue +++ b/apps/src/views/operate/gift-config.vue @@ -38,9 +38,10 @@ import { regionConfigTable, } from '#/api/legacy/system'; import AccountInput from '#/components/account-input.vue'; -import { copyText } from '#/views/operate/shared'; +import AdminConfigTable from '#/views/_shared/admin-config-table.vue'; import { formatDate, getAllowedSysOrigins } from '#/views/system/shared'; +import GiftBatchDrawer from './components/gift-batch-drawer.vue'; import GiftFormModal from './components/gift-form-modal.vue'; import { GIFT_CONFIG_TAB_OPTIONS } from './constants'; @@ -87,6 +88,7 @@ const settingQuery = reactive({ }); const giftFormOpen = ref(false); +const giftBatchOpen = ref(false); const activeGiftRow = ref>(null); const addRegionOpen = ref(false); const addRegionSaving = ref(false); @@ -130,7 +132,6 @@ const cpForm = reactive({ }); const settingColumns = [ - { dataIndex: 'id', key: 'id', title: 'ID', width: 120 }, { dataIndex: 'giftCode', key: 'giftCode', title: 'Code', width: 160 }, { dataIndex: 'giftPhoto', key: 'giftPhoto', title: '封面', width: 100 }, { dataIndex: 'giftName', key: 'giftName', title: '名称', width: 160 }, @@ -139,10 +140,10 @@ const settingColumns = [ { dataIndex: 'regionNameStr', key: 'regionNameStr', title: '区域', width: 180 }, { dataIndex: 'typeName', key: 'typeName', title: '类型', width: 120 }, { dataIndex: 'sort', key: 'sort', title: '排序权重', width: 100 }, - { dataIndex: 'del', key: 'del', title: '上/下架', width: 100 }, + { align: 'center' as const, className: 'switch-column', dataIndex: 'del', fixed: 'right' as const, key: 'del', title: '上/下架', width: 84 }, { dataIndex: 'createTime', key: 'createTime', title: '创建时间', width: 180 }, { dataIndex: 'userBaseInfo', key: 'userBaseInfo', title: '专属礼物归属人', width: 180 }, - { dataIndex: 'actions', key: 'actions', title: '操作', width: 180 }, + { align: 'center' as const, className: 'action-column', dataIndex: 'actions', fixed: 'right' as const, key: 'actions', title: '操作', width: 84 }, ]; const regionOptions = computed(() => @@ -249,20 +250,34 @@ function handleWeekStarPageChange(page: number, pageSize: number) { void loadWeekStar(); } -async function handleCopy(url?: string) { - if (!url) { - return; - } - await copyText(getAccessImgUrl(url)); - message.success('复制成功'); -} - async function handleSwitch(record: Record) { await switchDelStatus(record.id, record.del); message.success('操作成功'); await loadSetting(false); } +function stringifyCellValue(value: any) { + if (value === undefined || value === null || value === '') { + return '-'; + } + return String(value); +} + +function getColumnValue(record: Record, column: Record) { + const dataIndex = column?.dataIndex; + if (Array.isArray(dataIndex)) { + let value = record; + for (const key of dataIndex) { + value = value?.[String(key)]; + } + return value; + } + if (dataIndex === undefined || dataIndex === null) { + return undefined; + } + return record?.[String(dataIndex)]; +} + function openGiftCreate() { activeGiftRow.value = null; giftFormOpen.value = true; @@ -434,26 +449,30 @@ async function handleCpSave() { 新增礼物 + + 批量导入 + 添加全部礼物区域 - - - 复制 - @@ -473,19 +492,11 @@ async function handleCpSave() { 编辑 + + {{ stringifyCellValue(getColumnValue(record, column)) }} + - - - - - + @@ -588,6 +599,14 @@ async function handleCpSave() { @success="loadSetting(true)" /> + + = { animation: 'source', cover: 'cover', }; -const RESOURCE_NAME_TRANSLATION_ENTRIES = ([ - ['头像框', 'avatar frame'], - ['座驾', 'ride'], - ['坐骑', 'mount'], - ['勋章', 'badge'], - ['徽章', 'badge'], - ['管理员', 'admin'], - ['圣诞', 'christmas'], - ['新年', 'new year'], - ['情人节', 'valentine'], - ['万圣节', 'halloween'], - ['锦鲤', 'koi'], - ['熊猫', 'panda'], - ['独角兽', 'unicorn'], - ['天使', 'angel'], - ['恶魔', 'devil'], - ['皇冠', 'crown'], - ['国王', 'king'], - ['女王', 'queen'], - ['玫瑰', 'rose'], - ['星光', 'starlight'], - ['星星', 'star'], - ['月亮', 'moon'], - ['太阳', 'sun'], - ['彩虹', 'rainbow'], - ['火焰', 'flame'], - ['冰雪', 'snow'], - ['蓝色', 'blue'], - ['红色', 'red'], - ['紫色', 'purple'], - ['粉色', 'pink'], - ['金色', 'gold'], - ['银色', 'silver'], - ['黑色', 'black'], - ['白色', 'white'], - ['绿色', 'green'], - ['黄色', 'yellow'], - ['钻石', 'diamond'], - ['水晶', 'crystal'], - ['幸运', 'lucky'], - ['浪漫', 'romantic'], - ['荣耀', 'glory'], - ['守护', 'guard'], - ['贵族', 'noble'], - ['梦幻', 'dream'], - ['闪耀', 'shining'], - ['限定', 'limited'], - ['豪华', 'deluxe'], - ['高级', 'premium'], - ['经典', 'classic'], - ['热气球', 'hot air balloon'], - ['跑车', 'sports car'], - ['飞船', 'spaceship'], - ['游艇', 'yacht'], - ['摩托', 'motorcycle'], - ['龙', 'dragon'], - ['凤', 'phoenix'], - ['虎', 'tiger'], - ['兔', 'rabbit'], - ['猫', 'cat'], - ['狗', 'dog'], - ['鱼', 'fish'], -] as Array<[string, string]>).toSorted( - (current, next) => next[0].length - current[0].length, -); function isAmountRequiredByType(type: string) { return !['CUSTOMIZE', 'FRAGMENTS'].includes(type); @@ -348,43 +284,8 @@ function normalizeAmount(value: unknown) { return Number.isFinite(amount) && amount >= 0 ? amount : 0; } -function normalizeBaseName(value: string) { - const name = value - .replace(/\.[^.]+$/, '') - .replace(/([_-]?(cover|covers|source|resource|resources|ios|android|expand))$/i, '') - .replace(/([_-]?(封面|资源))$/i, '') - .trim(); - return name || value.replace(/\.[^.]+$/, '') || 'resource'; -} - function normalizeCode(value: string) { - const baseName = normalizeBaseName(value); - const translated = translateResourceName(baseName); - return slugifyResourceCode(translated) || slugifyResourceCode(baseName) || 'resource'; -} - -function translateResourceName(value: string) { - let result = value.trim(); - RESOURCE_NAME_TRANSLATION_ENTRIES.forEach(([source, target]) => { - result = result.replaceAll(source, ` ${target} `); - }); - return [...result] - .map((char) => { - if (/[\u4E00-\u9FFF]/.test(char)) { - return ` u${char.codePointAt(0)?.toString(16) || ''} `; - } - return char; - }) - .join(''); -} - -function slugifyResourceCode(value: string) { - return value - .normalize('NFKC') - .replaceAll(/[^\d A-Za-z]+/g, ' ') - .trim() - .replaceAll(/\s+/g, '_') - .toLowerCase(); + return normalizeResourceCode(value); } function getPathFileName(path: string) { diff --git a/apps/src/views/props/resource-config.vue b/apps/src/views/props/resource-config.vue index 2d00854..40694ea 100644 --- a/apps/src/views/props/resource-config.vue +++ b/apps/src/views/props/resource-config.vue @@ -2,8 +2,6 @@ import { computed, nextTick, - onBeforeUnmount, - onMounted, reactive, ref, } from 'vue'; @@ -19,16 +17,13 @@ import { useAccessStore } from '@vben/stores'; import { Button, - Card, Empty, Input, InputNumber, message, - Pagination, Popover, Select, Switch, - Table, Tooltip, } from 'antdv-next'; @@ -39,6 +34,7 @@ import { pagePropsSource, updatePropsSource, } from '#/api/legacy/props'; +import AdminConfigTable from '#/views/_shared/admin-config-table.vue'; import { getAllowedSysOrigins } from '#/views/system/shared'; import ResourceBatchDrawer from './components/resource-batch-drawer.vue'; @@ -80,20 +76,15 @@ const amountSavingId = ref(null); const amountDraft = ref(null); const filterOpen = reactive>({}); const pageActionOpen = ref(false); -const tableAreaRef = ref(null); -const pagerRef = ref(null); -const tableScrollY = ref(320); const amountFormatter = new Intl.NumberFormat('zh-CN', { maximumFractionDigits: 2, }); -const tableScroll = computed(() => ({ x: 1000, y: tableScrollY.value })); const adminFreeOptions = [ { label: '是', value: true as any }, { label: '否', value: false as any }, ]; const DEFAULT_STORE_CURRENCY_TYPES = 'GOLD'; const DEFAULT_STORE_VALID_DAYS = '7'; -let tableResizeObserver: null | ResizeObserver = null; const query = reactive({ cursor: 1, @@ -139,6 +130,21 @@ function stringifyCellValue(value: any) { return String(value); } +function getColumnValue(record: Record, column: Record) { + const dataIndex = column?.dataIndex; + if (Array.isArray(dataIndex)) { + let value = record; + for (const key of dataIndex) { + value = value?.[String(key)]; + } + return value; + } + if (dataIndex === undefined || dataIndex === null) { + return undefined; + } + return record?.[String(dataIndex)]; +} + function isFilterableColumn(key: unknown) { return filterableColumnKeys.has(String(key)); } @@ -264,7 +270,6 @@ async function loadData(reset = false) { total.value = result.total || 0; } finally { loading.value = false; - void refreshTableScroll(); } } @@ -289,25 +294,6 @@ async function attachStoreCommodities(records: Array>) { })); } -function updateTableScrollY() { - const area = tableAreaRef.value; - if (!area) { - return; - } - const headerHeight = - area.querySelector('.ant-table-thead')?.getBoundingClientRect().height || 48; - const scrollbarReserve = 8; - tableScrollY.value = Math.max( - 160, - Math.floor(area.clientHeight - headerHeight - scrollbarReserve), - ); -} - -async function refreshTableScroll() { - await nextTick(); - updateTableScrollY(); -} - function handlePageChange(page: number, pageSize: number) { query.cursor = page; query.limit = pageSize; @@ -499,28 +485,6 @@ function openEdit(record: Record) { formOpen.value = true; } -onMounted(async () => { - await nextTick(); - void refreshTableScroll(); - if (typeof ResizeObserver !== 'undefined') { - tableResizeObserver = new ResizeObserver(() => { - void refreshTableScroll(); - }); - if (tableAreaRef.value) { - tableResizeObserver.observe(tableAreaRef.value); - } - if (pagerRef.value) { - tableResizeObserver.observe(pagerRef.value); - } - } - window.addEventListener('resize', updateTableScrollY); -}); - -onBeforeUnmount(() => { - tableResizeObserver?.disconnect(); - window.removeEventListener('resize', updateTableScrollY); -}); - loadData(true); @@ -539,17 +503,18 @@ loadData(true); @success="loadData()" /> - - - + @@ -786,24 +751,14 @@ loadData(true); + + {{ stringifyCellValue(getColumnValue(record, column)) }} - - - - - - - - + + + - + tr > th) { - background: #fafafa; - border-bottom-color: rgb(237 240 244); - color: rgb(71 84 103); - font-size: var(--density-table-header-font-size); - font-weight: 500; - line-height: 1.35; - height: var(--density-table-header-height); - padding: 8px var(--density-table-cell-padding-x); -} - -.resource-table :deep(.ant-table-thead > tr > th::before) { - background-color: rgb(237 240 244) !important; -} - -.resource-table :deep(.ant-table-tbody > tr > td) { - border-bottom-color: rgb(237 240 244); - height: var(--density-table-row-height); - line-height: 1.35; - padding: 3px var(--density-table-cell-padding-x); -} - -.resource-table :deep(.ant-table-body) { - overflow-y: auto !important; -} - -.resource-table :deep(.switch-column) { - padding-left: var(--density-table-fixed-cell-padding-x) !important; - padding-right: var(--density-table-fixed-cell-padding-x) !important; -} - -.resource-table :deep(.action-column) { - padding-left: var(--density-table-fixed-cell-padding-x) !important; - padding-right: var(--density-table-fixed-cell-padding-x) !important; -} - -.resource-table :deep(.ant-table-tbody > tr:hover > td) { - background: rgb(250 252 255); -} - -.resource-table :deep(.ant-table-cell-fix-left), -.resource-table :deep(.ant-table-cell-fix-right) { - background: #fff; -} - -.resource-table :deep(.ant-table-tbody > tr:hover > .ant-table-cell-fix-left), -.resource-table :deep(.ant-table-tbody > tr:hover > .ant-table-cell-fix-right) { - background: rgb(250 252 255); } .compact-switch { diff --git a/apps/src/views/resident-activity/room-turnover-reward.vue b/apps/src/views/resident-activity/room-turnover-reward.vue index 7779684..fde10e8 100644 --- a/apps/src/views/resident-activity/room-turnover-reward.vue +++ b/apps/src/views/resident-activity/room-turnover-reward.vue @@ -75,6 +75,7 @@ const roomDetailsOpen = ref(false); const activeRoomId = ref(''); const form = reactive>({ + autoRewardEnabled: true, configured: false, enabled: false, id: 0, @@ -140,6 +141,7 @@ const enabledLevelCount = computed(() => function normalizeConfig(result: Record | null | undefined) { const value = result || {}; + form.autoRewardEnabled = value.autoRewardEnabled !== false; form.configured = Boolean(value.configured); form.enabled = Boolean(value.enabled); form.id = value.id || 0; @@ -282,6 +284,7 @@ async function submitForm() { saving.value = true; try { await saveRoomTurnoverRewardConfig({ + autoRewardEnabled: Boolean(form.autoRewardEnabled), enabled: Boolean(form.enabled), id: form.id || 0, levelConfigs: (form.levelConfigs || []).map((item: Record) => ({ @@ -386,6 +389,9 @@ watch( {{ form.enabled ? '已启用' : '已关闭' }} + + {{ form.autoRewardEnabled ? '自动发放' : '手动领取' }} + 按达到的最高档发放一次 刷新 @@ -394,6 +400,8 @@ watch( 启用 + 自动发放 + 结算时区 更新时间:{{ form.updateTime || '-' }} diff --git a/apps/src/views/resident-activity/voice-room-rocket.vue b/apps/src/views/resident-activity/voice-room-rocket.vue index 9fe9db5..4b4826e 100644 --- a/apps/src/views/resident-activity/voice-room-rocket.vue +++ b/apps/src/views/resident-activity/voice-room-rocket.vue @@ -1,5 +1,5 @@ @@ -786,14 +1013,98 @@ watch(rewardLevel, () => { class="field-control" /> - - 规则说明 JSON - - + + 规则说明富文本 + + + + + + + + + + + + + B + + + I + + + U + + + 无序列表 + + + 有序列表 + + + 清除格式 + + + + + + @@ -1152,6 +1463,76 @@ watch(rewardLevel, () => { width: 100%; } +.rule-editor-panel { + overflow: hidden; + width: 100%; + border: 1px solid #d9d9d9; + border-radius: 6px; + background: #fff; +} + +.rule-editor-toolbar { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; + justify-content: space-between; + padding: 8px; + border-bottom: 1px solid #f0f0f0; +} + +.rule-editor-locale { + width: 120px; +} + +.rule-editor-size { + width: 96px; +} + +.rule-editor-colors { + display: inline-flex; + gap: 6px; + align-items: center; +} + +.rule-color-swatch { + width: 22px; + height: 22px; + padding: 0; + cursor: pointer; + border: 1px solid #d9d9d9; + border-radius: 4px; +} + +.rule-color-swatch--active { + border-color: #1677ff; + box-shadow: 0 0 0 2px rgb(22 119 255 / 16%); +} + +.rule-color-picker { + width: 28px; + height: 24px; + padding: 0; + cursor: pointer; + border: 1px solid #d9d9d9; + border-radius: 4px; +} + +.rule-editor { + min-height: 180px; + max-height: 360px; + padding: 12px; + overflow: auto; + color: #111827; + outline: none; +} + +.rule-editor:empty::before { + color: #bfbfbf; + content: attr(data-placeholder); + pointer-events: none; +} + .actions { display: flex; justify-content: flex-end;