Full-project rename. `git grep -i aksrt` is now empty apart from the
DBMigrator 3.3.0 changelog note that documents the rename itself.
Code identifiers
- PHP namespace AKSRT\ -> Jio\ (composer PSR-4 + 378 declarations)
- global constants AKSRT_* -> JIO_* (VERSION/DIR/URI, PAY_TYPE_*, CAPTCHA_*)
- procedural functions, hooks, options, meta keys, table names,
cookies, query vars, ajax actions and nonces: aksrt_ -> jio_
- text domain aksrt -> jio; Theme Name -> Jio
- JS globals: aksrtData/aksrtAdminData/... -> jioData/jioAdminData/...,
window.AKSRTCaptcha & friends -> window.JioCaptcha, Aksrt* -> Jio*
- Vue components Aks*.vue -> Jio*.vue (15 files renamed + imports)
- CSS classes/data-attrs/custom properties: aksrt- -> jio-
- REST namespace aksrt/v1 -> jio/v1 (incl. payment gateway callbacks)
Storage layer
- DB tables {prefix}aksrt_* -> {prefix}jio_*; SCHEMA_VERSION 3.2.0 -> 3.3.0
so dbDelta creates the new names on install
- widget id_base aksrt_widget_* -> jio_widget_*
- uninstall.php drop list rebuilt from the actual migrator tables
(dropped 3 stale entries, added the 17 missing ones)
Removed
- hardcoded https://api.aksrt.com/version.json default update endpoint;
get_endpoint() now reads the 「更新端点」 setting and do_check() no-ops
when it is empty
Files renamed: inc/helpers/aksrt-*.php -> jio-*.php (54),
assets/admin/src/components/form/Aks*.vue -> Jio*.vue (15).
Build artifacts regenerated: assets/css/build, assets/js/runtime,
assets/admin/build.
Verified: audit-psr4-paths errors=0, audit-admin-contracts errors=0
(warnings=56, unchanged baseline), 813/813 widget-system tests,
44/44 phpunit, php -l clean across all PHP.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
204 lines
5.7 KiB
Vue
204 lines
5.7 KiB
Vue
<template>
|
||
<n-card class="jio-section-card">
|
||
<div class="jio-section-header">
|
||
<h3>预设回复</h3>
|
||
<n-space>
|
||
<n-button size="small" :disabled="saving || !reorderable" :loading="savingOrder" @click="saveOrder">
|
||
保存排序
|
||
</n-button>
|
||
<n-button size="small" type="primary" :disabled="saving" @click="openCreate">新增预设回复</n-button>
|
||
</n-space>
|
||
</div>
|
||
|
||
<n-alert type="info" :show-icon="false" style="margin-bottom: 12px;">
|
||
预设回复是管理员回复框的一键插入话术,按下列顺序展示;拖动行调整顺序后需点「保存排序」。
|
||
</n-alert>
|
||
|
||
<n-empty v-if="!rows.length && !loading" description="暂无预设回复" />
|
||
|
||
<draggable
|
||
v-else
|
||
v-model="rows"
|
||
v-bind="SORTABLE_LIST_ATTRS"
|
||
item-key="id"
|
||
class="jio-sortable"
|
||
>
|
||
<template #item="{ element, index }">
|
||
<div class="jio-sortable__item">
|
||
<div class="jio-sortable__head">
|
||
<JioSortHandle />
|
||
<span class="jio-sortable__index">{{ index + 1 }}</span>
|
||
<div class="jio-sortable__main">
|
||
<span class="jio-ticket-prereply__content">{{ element.content }}</span>
|
||
</div>
|
||
<div class="jio-sortable__actions">
|
||
<n-button size="tiny" type="primary" quaternary @click="openEdit(element)">编辑</n-button>
|
||
<n-button size="tiny" type="error" quaternary @click="handleDelete(element)">删除</n-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</draggable>
|
||
|
||
<n-modal
|
||
v-model:show="visible"
|
||
:title="editing ? '编辑预设回复' : '新增预设回复'"
|
||
preset="card"
|
||
style="width: 520px; max-width: 92vw;"
|
||
:mask-closable="false"
|
||
>
|
||
<n-form ref="formRef" :model="form" :rules="rules" label-placement="top">
|
||
<n-form-item label="回复内容" path="content">
|
||
<n-input
|
||
v-model:value="form.content"
|
||
type="textarea"
|
||
:rows="5"
|
||
placeholder="填写可一键插入的回复话术"
|
||
maxlength="2000"
|
||
show-count
|
||
/>
|
||
</n-form-item>
|
||
</n-form>
|
||
<template #footer>
|
||
<n-space justify="end">
|
||
<n-button :disabled="saving" @click="visible = false">取消</n-button>
|
||
<n-button type="primary" :loading="saving" @click="handleSubmit">保存</n-button>
|
||
</n-space>
|
||
</template>
|
||
</n-modal>
|
||
</n-card>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, onMounted, ref } from 'vue'
|
||
import { NButton, NSpace, useDialog, useMessage } from 'naive-ui'
|
||
import draggable from 'vuedraggable'
|
||
import api from '@/api'
|
||
import JioSortHandle from '@/components/form/JioSortHandle.vue'
|
||
import { SORTABLE_LIST_ATTRS } from '@/components/form/sortable-options.js'
|
||
|
||
const message = useMessage()
|
||
const dialog = useDialog()
|
||
|
||
const loading = ref(false)
|
||
const saving = ref(false)
|
||
const savingOrder = ref(false)
|
||
const visible = ref(false)
|
||
const formRef = ref(null)
|
||
const editing = ref(null)
|
||
const rows = ref([])
|
||
/** 初始顺序,用于判断排序是否需要保存。 */
|
||
const baseline = ref([])
|
||
|
||
const form = ref({ content: '' })
|
||
|
||
const rules = {
|
||
content: [
|
||
{ required: true, message: '请填写预设回复内容', trigger: 'blur' },
|
||
{ max: 2000, message: '内容不能超过 2000 字', trigger: 'blur' },
|
||
],
|
||
}
|
||
|
||
const reorderable = computed(
|
||
() => rows.value.length > 1 && baseline.value.join(',') !== rows.value.map((r) => r.id).join(',')
|
||
)
|
||
|
||
async function load() {
|
||
loading.value = true
|
||
try {
|
||
const res = await api.listTicketPrereplies()
|
||
rows.value = res?.data || []
|
||
baseline.value = rows.value.map((row) => row.id)
|
||
} catch (error) {
|
||
message.error(error?.message || '加载预设回复失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function openCreate() {
|
||
editing.value = null
|
||
form.value = { content: '' }
|
||
visible.value = true
|
||
}
|
||
|
||
function openEdit(row) {
|
||
editing.value = row
|
||
form.value = { content: String(row.content || '') }
|
||
visible.value = true
|
||
}
|
||
|
||
async function handleSubmit() {
|
||
try {
|
||
await formRef.value?.validate()
|
||
} catch {
|
||
return
|
||
}
|
||
saving.value = true
|
||
try {
|
||
const content = form.value.content.trim()
|
||
if (editing.value) {
|
||
await api.updateTicketPrereply(editing.value.id, { content })
|
||
message.success('预设回复已保存')
|
||
} else {
|
||
await api.createTicketPrereply({ content })
|
||
message.success('预设回复已创建')
|
||
}
|
||
visible.value = false
|
||
await load()
|
||
} catch (error) {
|
||
message.error(error?.message || '保存预设回复失败')
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
function handleDelete(row) {
|
||
dialog.warning({
|
||
title: '删除预设回复',
|
||
content: '确认删除该预设回复吗?',
|
||
positiveText: '删除',
|
||
negativeText: '取消',
|
||
onPositiveClick: async () => {
|
||
try {
|
||
await api.deleteTicketPrereply(row.id)
|
||
message.success('预设回复已删除')
|
||
await load()
|
||
} catch (error) {
|
||
message.error(error?.message || '删除预设回复失败')
|
||
}
|
||
},
|
||
})
|
||
}
|
||
|
||
/** 排序接口要求全量 ID,数量不符后端会拒绝。 */
|
||
async function saveOrder() {
|
||
const ids = rows.value.map((row) => row.id)
|
||
savingOrder.value = true
|
||
try {
|
||
await api.reorderTicketPrereplies(ids)
|
||
baseline.value = [...ids]
|
||
message.success('排序已保存')
|
||
} catch (error) {
|
||
message.error(error?.message || '保存排序失败')
|
||
await load()
|
||
} finally {
|
||
savingOrder.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(load)
|
||
|
||
defineExpose({ reload: load })
|
||
</script>
|
||
|
||
<style scoped>
|
||
.jio-ticket-prereply__content {
|
||
color: var(--jio-color-text, #1F2923);
|
||
font-size: 13px;
|
||
line-height: 1.6;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
}
|
||
</style>
|