用户原话: 不是让你沿用老vue页面内容了吗, 只是更换成新版本的UI 用户原话: 要能够交付使用, 像原有的vue平台一样使用 整改 (5 个 view 从严重简化变完整版): - OrderListView 240 行: 订单号/合同号/供应商/状态/时间范围 完整搜索, 8 种状态 tab, 操作 (详情/付款/发货/收货/退款/取消/打印), 批量导出, type 自适应 good/service/group - OrderDetailView 582 行: 完整 4-tab (订单信息/商品明细/物流/付款发票), 状态卡 + 操作按钮 (按状态动态显示), 9 种订单状态, 推广信息, 多状态按需渲染 - GoodListView 507 行: 12 列表格 (封面/商品/供应商/3 级分类/品牌/单位/铺货/审核/状态), 4 种状态 tab, 5 种操作 (详情/审核/上下架/改分类/删除), 5 种批量操作, 详情弹窗含 SKU 列表 (编码/原价/现价/秒杀价/拼团价/库存) - ServiceGoodListView 302 行: 同上针对服务商品 - InquiryDetailView 229 行: 询价基本信息 (产品/品牌/规格/数量/收货地址/交货日期/价格有效), 附件下载, 报价列表 (供应商/状态/金额/次数/来源/查看报价) 字段 100% 沿用老 vue (supplierGoodId/contractCode/groupNumber/acceptNum 等), 新增 mock 数据 (3-8 条覆盖各种业务场景), 弹窗/操作/批量/导出 全部走通 (mock 实现) + W3.x C P0-P2 累计 30+ 个 view 文件补 commit (inquiry/openapi/order/sys 等 11+ 模块) router 调整: 9 个 order 路由统一到 OrderListView (type 自动推断) + /supplier/servicegood 路由独立 (避免被 tree-shake)
482 lines
16 KiB
Vue
482 lines
16 KiB
Vue
<script setup lang="ts">
|
|
/**
|
|
* F006 集成管理 - 申请 APIKey 向导(3 步)
|
|
* W4.1-revert v2: 用户原话"申请 apikey 是三步操作:选客户、填写信息、生成 apikey"
|
|
* Step 1: 选择客户(多选 + 搜索过滤)
|
|
* Step 2: 填写信息(appName/appType/scopes/ipWhitelist/remark,共 5 字段,可选)
|
|
* Step 3: 生成 APIKey(确认摘要 + 生成结果表格)
|
|
*
|
|
* 后端 DTO: OpenApiAppApplyDTO
|
|
* - customerCodes(必填,Step 1 选)
|
|
* - appName(可选,Step 2)
|
|
* - appType(可选,Step 2)
|
|
* - scopes(可选,Step 2)
|
|
* - ipWhitelist(可选,Step 2)
|
|
* - remark(可选,Step 2)
|
|
*
|
|
* 后端调 /sys/openapi/app/apply
|
|
*/
|
|
import { ref, reactive, computed, watch } from 'vue'
|
|
import { ElMessage } from 'element-plus'
|
|
import { httpGet, httpPost } from '@/utils/request'
|
|
|
|
interface Customer {
|
|
customerCode: string
|
|
customerName: string
|
|
contactName?: string
|
|
contactPhone?: string
|
|
currentAppCount?: number
|
|
}
|
|
|
|
interface ApplyResult {
|
|
customerName: string
|
|
customerCode: string
|
|
appId: string
|
|
connectCode: string
|
|
appSecret: string
|
|
expireAt: string
|
|
}
|
|
|
|
const props = defineProps<{ modelValue: boolean }>()
|
|
const emit = defineEmits<{
|
|
(e: 'update:modelValue', v: boolean): void
|
|
(e: 'refresh'): void
|
|
}>()
|
|
|
|
const visible = ref(props.modelValue)
|
|
watch(() => props.modelValue, v => {
|
|
visible.value = v
|
|
if (v) { reset() }
|
|
})
|
|
function updateVisible(v: boolean) {
|
|
visible.value = v
|
|
emit('update:modelValue', v)
|
|
}
|
|
|
|
const step = ref<1 | 2 | 3>(1)
|
|
const submitting = ref(false)
|
|
const result = ref<ApplyResult[] | null>(null)
|
|
const customerTableRef = ref()
|
|
|
|
const form = reactive({
|
|
searchKey: '',
|
|
selected: [] as Customer[],
|
|
appName: '',
|
|
appType: 'both' as 'mcp' | 'rest' | 'both',
|
|
scopes: [] as string[],
|
|
ipWhitelist: '',
|
|
remark: ''
|
|
})
|
|
|
|
const customers = ref<Customer[]>([])
|
|
const loadingCustomers = ref(false)
|
|
|
|
async function loadCustomers() {
|
|
loadingCustomers.value = true
|
|
try {
|
|
const data = await httpGet<Customer[]>('/sys/openapi/customer/list')
|
|
customers.value = Array.isArray(data) ? data : []
|
|
} catch (e: any) {
|
|
ElMessage.error(e?.msg || e?.message || '加载客户列表失败')
|
|
customers.value = []
|
|
} finally {
|
|
loadingCustomers.value = false
|
|
}
|
|
}
|
|
|
|
watch(visible, v => { if (v) loadCustomers() })
|
|
|
|
const filteredCustomers = computed(() => {
|
|
if (!form.searchKey) return customers.value
|
|
const k = form.searchKey.toLowerCase()
|
|
return customers.value.filter(c =>
|
|
(c.customerName || '').toLowerCase().includes(k) ||
|
|
(c.customerCode || '').toLowerCase().includes(k)
|
|
)
|
|
})
|
|
|
|
function onSelectionChange(rows: Customer[]) {
|
|
form.selected = rows
|
|
}
|
|
|
|
const SCOPE_OPTIONS = [
|
|
{ value: 'mcp:*', label: 'MCP 全部权限' },
|
|
{ value: 'rest:*', label: 'REST 全部权限' },
|
|
{ value: 'webhook:read', label: 'Webhook 读取' },
|
|
{ value: 'webhook:write', label: 'Webhook 写入' },
|
|
{ value: 'inquiry:read', label: '询价读取' },
|
|
{ value: 'inquiry:write', label: '询价写入' },
|
|
{ value: 'order:read', label: '订单读取' },
|
|
{ value: 'order:write', label: '订单写入' }
|
|
]
|
|
|
|
async function submit() {
|
|
if (form.selected.length === 0) {
|
|
ElMessage.warning('请至少选择一个客户')
|
|
return
|
|
}
|
|
submitting.value = true
|
|
try {
|
|
const payload: any = {
|
|
customerCodes: form.selected.map(c => c.customerCode)
|
|
}
|
|
if (form.appName) payload.appName = form.appName
|
|
if (form.appType) payload.appType = form.appType
|
|
if (form.scopes.length > 0) payload.scopes = form.scopes
|
|
if (form.ipWhitelist) payload.ipWhitelist = form.ipWhitelist
|
|
if (form.remark) payload.remark = form.remark
|
|
|
|
const res = await httpPost<ApplyResult[]>('/sys/openapi/app/apply', payload)
|
|
result.value = Array.isArray(res) ? res : []
|
|
ElMessage.success(`已为 ${result.value.length} 个客户生成 APIKey`)
|
|
emit('refresh')
|
|
} catch (e: any) {
|
|
ElMessage.error(e?.msg || e?.message || '申请失败')
|
|
} finally {
|
|
submitting.value = false
|
|
}
|
|
}
|
|
|
|
function onClose() { updateVisible(false) }
|
|
|
|
async function copySecret(text: string) {
|
|
try {
|
|
await navigator.clipboard.writeText(text)
|
|
ElMessage.success('已复制')
|
|
} catch {
|
|
ElMessage.error('复制失败,请手动选择文本')
|
|
}
|
|
}
|
|
|
|
function reset() {
|
|
step.value = 1
|
|
result.value = null
|
|
form.searchKey = ''
|
|
form.selected = []
|
|
form.appName = ''
|
|
form.appType = 'both'
|
|
form.scopes = []
|
|
form.ipWhitelist = ''
|
|
form.remark = ''
|
|
}
|
|
|
|
function nextStep() {
|
|
if (step.value === 1 && form.selected.length === 0) {
|
|
ElMessage.warning('请至少选择一个客户')
|
|
return
|
|
}
|
|
if (step.value < 3) {
|
|
step.value = (step.value + 1) as 1 | 2 | 3
|
|
}
|
|
}
|
|
function prevStep() {
|
|
if (step.value > 1) {
|
|
step.value = (step.value - 1) as 1 | 2 | 3
|
|
}
|
|
}
|
|
|
|
function notifyContact() {
|
|
ElMessage.success('已通知联系人(占位)')
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<el-dialog
|
|
:model-value="visible"
|
|
@update:model-value="updateVisible"
|
|
title="申请 APIKey"
|
|
width="640px"
|
|
:close-on-click-modal="false"
|
|
@close="onClose"
|
|
class="rr-apply-dialog"
|
|
>
|
|
<!-- 步骤条(3 步:选择客户 → 填写信息 → 生成 APIKey) -->
|
|
<div class="rr-wizard-steps">
|
|
<div :class="['rr-wizard-step', step >= 1 ? 'active' : '', step > 1 ? 'done' : '']">
|
|
<span class="rr-wizard-num">1</span>
|
|
<span class="rr-wizard-label">选择客户</span>
|
|
</div>
|
|
<div :class="['rr-wizard-step', step >= 2 ? 'active' : '', step > 2 ? 'done' : '']">
|
|
<span class="rr-wizard-num">2</span>
|
|
<span class="rr-wizard-label">填写信息</span>
|
|
</div>
|
|
<div :class="['rr-wizard-step', step >= 3 ? 'active' : '']">
|
|
<span class="rr-wizard-num">3</span>
|
|
<span class="rr-wizard-label">生成 APIKey</span>
|
|
</div>
|
|
</div>
|
|
|
|
<el-alert v-if="step === 1" type="info" :closable="false" style="margin-bottom: 16px;">
|
|
可选择多个客户,后端会为每个客户生成独立的 APIKey + 公司账号
|
|
</el-alert>
|
|
<el-alert v-if="step === 2" type="info" :closable="false" style="margin-bottom: 16px;">
|
|
以下信息均可选,不填则使用后端默认值
|
|
</el-alert>
|
|
|
|
<!-- Step 1: 选客户 -->
|
|
<div v-if="step === 1">
|
|
<el-input
|
|
v-model="form.searchKey"
|
|
placeholder="搜索客户名称 / 客户编码"
|
|
clearable
|
|
size="small"
|
|
style="width: 280px; margin-bottom: 10px;"
|
|
/>
|
|
<el-table
|
|
ref="customerTableRef"
|
|
:data="filteredCustomers"
|
|
border
|
|
size="small"
|
|
max-height="320"
|
|
:loading="loadingCustomers"
|
|
:row-style="{ height: '36px' }"
|
|
:cell-style="{ padding: '4px 8px', fontSize: '12.5px' }"
|
|
:header-cell-style="{ padding: '6px 8px', fontSize: '12px', height: '32px', background: '#F8FAFC' }"
|
|
@selection-change="onSelectionChange"
|
|
>
|
|
<el-table-column type="selection" header-align="center" align="center" width="40" />
|
|
<el-table-column prop="customerName" label="客户名" min-width="180" header-align="center" align="center" show-overflow-tooltip />
|
|
<el-table-column prop="contactName" label="联系人" width="100" align="center" />
|
|
<el-table-column prop="contactPhone" label="联系电话" width="130" align="center" />
|
|
</el-table>
|
|
</div>
|
|
|
|
<!-- Step 2: 填写信息(5 字段表单,均可选) -->
|
|
<div v-if="step === 2">
|
|
<el-form :model="form" label-width="120px" label-position="right">
|
|
<el-form-item label="应用名称">
|
|
<el-input
|
|
v-model="form.appName"
|
|
placeholder="不填则后端自动生成(客户名 + 智能体集成)"
|
|
maxlength="50"
|
|
show-word-limit
|
|
/>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="应用类型">
|
|
<el-radio-group v-model="form.appType">
|
|
<el-radio value="mcp">MCP(Model Context Protocol)</el-radio>
|
|
<el-radio value="rest">REST API</el-radio>
|
|
<el-radio value="both">Both(MCP + REST)</el-radio>
|
|
</el-radio-group>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="权限范围">
|
|
<el-select
|
|
v-model="form.scopes"
|
|
multiple
|
|
collapse-tags
|
|
collapse-tags-tooltip
|
|
placeholder="不选则后端默认全权限"
|
|
style="width: 100%;"
|
|
>
|
|
<el-option
|
|
v-for="s in SCOPE_OPTIONS"
|
|
:key="s.value"
|
|
:label="s.label"
|
|
:value="s.value"
|
|
/>
|
|
</el-select>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="IP 白名单">
|
|
<el-input
|
|
v-model="form.ipWhitelist"
|
|
type="textarea"
|
|
:rows="2"
|
|
placeholder="多 IP 用英文逗号分隔,如 192.168.1.1,10.0.0.0/24。不填则不限制 IP"
|
|
/>
|
|
</el-form-item>
|
|
|
|
<el-form-item label="备注">
|
|
<el-input
|
|
v-model="form.remark"
|
|
type="textarea"
|
|
:rows="2"
|
|
placeholder="申请原因 / 用途说明(选填)"
|
|
maxlength="200"
|
|
show-word-limit
|
|
/>
|
|
</el-form-item>
|
|
</el-form>
|
|
</div>
|
|
|
|
<!-- Step 3: 确认 + 生成 -->
|
|
<div v-if="step === 3 && !result">
|
|
<h3 style="margin: 0 0 16px 0;">确认信息</h3>
|
|
<el-descriptions :column="1" border>
|
|
<el-descriptions-item label="已选客户">
|
|
<span v-for="c in form.selected" :key="c.customerCode" class="rr-customer-tag">{{ c.customerName }}</span>
|
|
</el-descriptions-item>
|
|
<el-descriptions-item label="应用名称">{{ form.appName || '(后端自动生成)' }}</el-descriptions-item>
|
|
<el-descriptions-item label="应用类型">{{ form.appType }}</el-descriptions-item>
|
|
<el-descriptions-item label="权限范围">
|
|
<span v-if="form.scopes.length === 0">(后端默认全权限)</span>
|
|
<span v-else>
|
|
<span v-for="s in form.scopes" :key="s" class="rr-scope-tag">{{ s }}</span>
|
|
</span>
|
|
</el-descriptions-item>
|
|
<el-descriptions-item label="IP 白名单">{{ form.ipWhitelist || '(不限制)' }}</el-descriptions-item>
|
|
<el-descriptions-item label="备注">{{ form.remark || '—' }}</el-descriptions-item>
|
|
</el-descriptions>
|
|
<el-alert type="info" :closable="false" style="margin-top: 16px;">
|
|
后端将自动创建公司账号 + 生成 connect_code(2h 有效)
|
|
</el-alert>
|
|
</div>
|
|
|
|
<!-- Step 3: 生成结果 -->
|
|
<div v-if="step === 3 && result">
|
|
<el-alert type="warning" :closable="false" style="margin-bottom: 16px;">
|
|
请立即复制 connectCode 和 appSecret!关弹窗后无法再次查看
|
|
</el-alert>
|
|
<p>已为 {{ result.length }} 个客户生成 APIKey:</p>
|
|
<el-table :data="result" border>
|
|
<el-table-column prop="customerName" label="客户名" width="160" />
|
|
<el-table-column prop="appId" label="AppID" width="280">
|
|
<template #default="{ row }">
|
|
<code>{{ row.appId }}</code>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="connectCode" label="ConnectCode">
|
|
<template #default="{ row }">
|
|
<code class="rr-code-accent">{{ row.connectCode }}</code>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="expireAt" label="过期时间" width="180" />
|
|
<el-table-column label="AppSecret" min-width="320">
|
|
<template #default="{ row }">
|
|
<el-input :model-value="row.appSecret" readonly type="password" size="small" show-password style="width: 220px; margin-right: 8px;" />
|
|
<el-button size="small" type="primary" @click="copySecret(row.appSecret)">复制</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
<el-alert type="warning" :closable="false" style="margin-top: 16px;">
|
|
AppSecret 仅在此次显示,关弹窗后无法再次查看
|
|
</el-alert>
|
|
</div>
|
|
|
|
<template #footer>
|
|
<template v-if="step === 1">
|
|
<el-button @click="onClose">取消</el-button>
|
|
<el-button type="primary" :disabled="form.selected.length === 0" @click="nextStep">下一步 →</el-button>
|
|
</template>
|
|
<template v-else-if="step === 2 && !result">
|
|
<el-button @click="prevStep">← 上一步</el-button>
|
|
<el-button @click="onClose">取消</el-button>
|
|
<el-button type="primary" @click="nextStep">下一步 →</el-button>
|
|
</template>
|
|
<template v-else-if="step === 3 && !result">
|
|
<el-button @click="prevStep">← 上一步</el-button>
|
|
<el-button type="primary" :loading="submitting" @click="submit">生成 APIKey</el-button>
|
|
</template>
|
|
<template v-else>
|
|
<el-button type="primary" @click="notifyContact">通知联系人</el-button>
|
|
<el-button @click="onClose">关闭并刷新</el-button>
|
|
</template>
|
|
</template>
|
|
</el-dialog>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
/* 弹窗圆角 12px + 紧凑 padding(套设计规范 §7.1 + §17 v3) */
|
|
:deep(.rr-apply-dialog .el-dialog) {
|
|
border-radius: 12px !important;
|
|
overflow: hidden;
|
|
}
|
|
:deep(.rr-apply-dialog .el-dialog__header) {
|
|
padding: 14px 20px;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
:deep(.rr-apply-dialog .el-dialog__header .el-dialog__title) {
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
}
|
|
:deep(.rr-apply-dialog .el-dialog__body) {
|
|
padding: 16px 20px;
|
|
}
|
|
:deep(.rr-apply-dialog .el-dialog__footer) {
|
|
padding: 12px 20px;
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
|
|
/* 输入框圆角 8px + 紧凑(套设计规范 §11) */
|
|
:deep(.rr-apply-dialog .el-input__wrapper),
|
|
:deep(.rr-apply-dialog .el-textarea__inner),
|
|
:deep(.rr-apply-dialog .el-select__wrapper) {
|
|
border-radius: 8px !important;
|
|
}
|
|
|
|
/* 弹窗内 el-alert 紧凑 */
|
|
:deep(.rr-apply-dialog .el-alert) {
|
|
padding: 6px 12px;
|
|
}
|
|
:deep(.rr-apply-dialog .el-alert__content) {
|
|
padding: 0;
|
|
}
|
|
:deep(.rr-apply-dialog .el-alert .el-alert__title) {
|
|
font-size: 12px;
|
|
font-weight: 400;
|
|
}
|
|
|
|
/* 弹窗内 h3 紧凑 */
|
|
:deep(.rr-apply-dialog h3) {
|
|
font-size: 13px !important;
|
|
font-weight: 600 !important;
|
|
margin: 0 0 10px 0 !important;
|
|
}
|
|
|
|
/* 步骤条(紧凑版) */
|
|
.rr-wizard-steps { display: flex; margin-bottom: 16px; }
|
|
.rr-wizard-step {
|
|
flex: 1; text-align: center; position: relative;
|
|
padding: 0 8px;
|
|
}
|
|
.rr-wizard-num {
|
|
width: 26px; height: 26px; line-height: 24px;
|
|
border-radius: 50%; background: var(--surface);
|
|
border: 1.5px solid var(--border);
|
|
display: inline-block; color: var(--ink-3);
|
|
font-size: 12.5px; font-weight: 500;
|
|
transition: all 0.2s ease;
|
|
}
|
|
.rr-wizard-step.active .rr-wizard-num {
|
|
background: var(--brand);
|
|
border-color: var(--brand);
|
|
color: white;
|
|
box-shadow: 0 0 0 3px var(--brand-soft);
|
|
}
|
|
.rr-wizard-step.done .rr-wizard-num {
|
|
background: var(--green);
|
|
border-color: var(--green);
|
|
color: white;
|
|
}
|
|
.rr-wizard-label { display: block; margin-top: 6px; color: var(--ink-3); font-size: 12px; }
|
|
.rr-wizard-step.active .rr-wizard-label { color: var(--brand); font-weight: 500; }
|
|
.rr-wizard-step.done .rr-wizard-label { color: var(--green); font-weight: 500; }
|
|
|
|
/* 步骤连接线(已完成的步骤变绿色) */
|
|
.rr-wizard-step:not(:last-child)::after {
|
|
content: ""; position: absolute;
|
|
top: 13px; left: calc(50% + 16px); right: calc(-50% + 16px);
|
|
height: 1.5px; background: var(--border); z-index: -1;
|
|
}
|
|
.rr-wizard-step.done:not(:last-child)::after { background: var(--green); }
|
|
|
|
.rr-customer-tag {
|
|
display: inline-block; padding: 3px 10px;
|
|
background: var(--brand-soft); color: var(--brand);
|
|
border-radius: 6px; margin-right: 8px; margin-bottom: 4px;
|
|
font-size: 12.5px;
|
|
}
|
|
|
|
.rr-scope-tag {
|
|
display: inline-block; padding: 2px 8px;
|
|
background: var(--surface-2); color: var(--ink-2);
|
|
border-radius: 4px; margin-right: 6px;
|
|
font-family: ui-monospace, monospace;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.rr-code-accent { color: var(--red); font-family: ui-monospace, monospace; }
|
|
</style>
|