Compare commits
10 Commits
9794bec2ae
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da906d52c0 | ||
|
|
bc33717921 | ||
|
|
2b9e1aef3f | ||
|
|
60c921ea28 | ||
|
|
9f18f13607 | ||
|
|
27c70ac567 | ||
|
|
4fba24438c | ||
|
|
bf092e5f69 | ||
|
|
d680da2d83 | ||
|
|
85ec563242 |
@@ -27,3 +27,12 @@ export function generateComplete(data) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 闲鱼文案(手动):根据标题+可选型号生成代下单、教你下单文案
|
||||||
|
export function generateXianyuWenan(data) {
|
||||||
|
return request({
|
||||||
|
url: '/jarvis/social-media/xianyu-wenan/generate',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,21 @@ export function executeInstructionWithForce(data) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getHistory(type, limit) {
|
/**
|
||||||
|
* 获取历史消息记录
|
||||||
|
* @param type request | response
|
||||||
|
* @param limit 条数上限
|
||||||
|
* @param keyword 可选,搜索关键词;传则后端在全部数据中过滤后返回
|
||||||
|
*/
|
||||||
|
export function getHistory(type, limit, keyword) {
|
||||||
|
const params = { type, limit }
|
||||||
|
if (keyword != null && String(keyword).trim() !== '') {
|
||||||
|
params.keyword = String(keyword).trim()
|
||||||
|
}
|
||||||
return request({
|
return request({
|
||||||
url: '/jarvis/instruction/history',
|
url: '/jarvis/instruction/history',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params: { type, limit }
|
params
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,82 +34,20 @@ export default {
|
|||||||
default: true
|
default: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
navItemsCache: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['device', 'sidebarRouters']),
|
...mapGetters(['device', 'sidebarRouters']),
|
||||||
isMobile() {
|
isMobile() {
|
||||||
return this.device === 'mobile' || window.innerWidth < 768
|
return this.device === 'mobile' || window.innerWidth < 768
|
||||||
},
|
},
|
||||||
navItems() {
|
navItems() {
|
||||||
// 如果提供了自定义items,直接使用
|
// 如果提供了自定义 items,直接使用
|
||||||
if (this.items && this.items.length > 0) {
|
if (this.items && this.items.length > 0) {
|
||||||
return this.items
|
return this.items
|
||||||
}
|
}
|
||||||
|
// 始终从 store 的 sidebarRouters 计算,保证与接口返回、路由注册一致,避免移动端菜单/跳转错乱
|
||||||
// 使用缓存,避免重复计算
|
|
||||||
if (this.navItemsCache) {
|
|
||||||
return this.navItemsCache
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从侧边栏路由中获取可用的路由
|
|
||||||
const routes = this.sidebarRouters || []
|
const routes = this.sidebarRouters || []
|
||||||
|
const flatRoutes = this.flattenRoutes(routes)
|
||||||
// 扁平化路由,获取所有叶子节点路由
|
const mainRoutes = flatRoutes.filter(route => {
|
||||||
const flattenRoutes = (routes, parentPath = '') => {
|
|
||||||
let result = []
|
|
||||||
if (!routes || !Array.isArray(routes)) {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
routes.forEach(route => {
|
|
||||||
if (route.hidden) return
|
|
||||||
|
|
||||||
// 处理路径 - 确保路径正确
|
|
||||||
let fullPath = route.path || ''
|
|
||||||
if (parentPath) {
|
|
||||||
if (fullPath.startsWith('/')) {
|
|
||||||
fullPath = fullPath
|
|
||||||
} else {
|
|
||||||
// 合并路径
|
|
||||||
const basePath = parentPath.endsWith('/') ? parentPath.slice(0, -1) : parentPath
|
|
||||||
fullPath = `${basePath}/${fullPath}`.replace(/\/+/g, '/')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确保路径以/开头
|
|
||||||
if (fullPath && !fullPath.startsWith('/')) {
|
|
||||||
fullPath = '/' + fullPath
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果有子路由,递归处理
|
|
||||||
if (route.children && route.children.length > 0) {
|
|
||||||
result = result.concat(flattenRoutes(route.children, fullPath))
|
|
||||||
} else {
|
|
||||||
// 叶子节点路由,且有meta信息
|
|
||||||
if (route.meta && route.meta.title && fullPath) {
|
|
||||||
result.push({
|
|
||||||
path: fullPath,
|
|
||||||
label: route.meta.title,
|
|
||||||
icon: route.meta.icon || 'el-icon-menu',
|
|
||||||
iconClass: route.meta.icon,
|
|
||||||
route: route
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
const flatRoutes = flattenRoutes(routes)
|
|
||||||
|
|
||||||
// 过滤并获取所有主要路由(不限制数量,显示所有)
|
|
||||||
const mainRoutes = flatRoutes
|
|
||||||
.filter(route => {
|
|
||||||
// 过滤掉一些特殊路由
|
|
||||||
const excludePaths = ['/redirect', '/login', '/register', '/404', '/401', '/user/profile']
|
const excludePaths = ['/redirect', '/login', '/register', '/404', '/401', '/user/profile']
|
||||||
const path = route.path || ''
|
const path = route.path || ''
|
||||||
return path &&
|
return path &&
|
||||||
@@ -117,47 +55,47 @@ export default {
|
|||||||
!excludePaths.some(exclude => path.includes(exclude)) &&
|
!excludePaths.some(exclude => path.includes(exclude)) &&
|
||||||
!path.startsWith('/user/')
|
!path.startsWith('/user/')
|
||||||
})
|
})
|
||||||
// 不限制数量,显示所有可用路由
|
|
||||||
|
|
||||||
// 缓存结果
|
|
||||||
if (mainRoutes.length > 0) {
|
if (mainRoutes.length > 0) {
|
||||||
this.navItemsCache = mainRoutes
|
|
||||||
return mainRoutes
|
return mainRoutes
|
||||||
}
|
}
|
||||||
|
return [
|
||||||
// 如果没有找到路由,返回默认导航
|
{ path: '/sloworder/index', label: '首页', icon: 'el-icon-s-home' }
|
||||||
const defaultRoutes = [
|
|
||||||
{
|
|
||||||
path: '/sloworder/index',
|
|
||||||
label: '首页',
|
|
||||||
icon: 'el-icon-s-home'
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
|
|
||||||
this.navItemsCache = defaultRoutes
|
|
||||||
return defaultRoutes
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
|
||||||
sidebarRouters: {
|
|
||||||
handler() {
|
|
||||||
// 路由变化时清除缓存
|
|
||||||
this.navItemsCache = null
|
|
||||||
},
|
|
||||||
deep: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
// 等待路由加载完成
|
|
||||||
this.$nextTick(() => {
|
|
||||||
// 延迟一下,确保路由已经加载
|
|
||||||
setTimeout(() => {
|
|
||||||
this.navItemsCache = null
|
|
||||||
this.$forceUpdate()
|
|
||||||
}, 500)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
|
/** 扁平化路由为叶子节点,路径与 Vue Router 注册的完整 path 一致 */
|
||||||
|
flattenRoutes(routes, parentPath = '') {
|
||||||
|
if (!routes || !Array.isArray(routes)) return []
|
||||||
|
const result = []
|
||||||
|
routes.forEach(route => {
|
||||||
|
if (route.hidden) return
|
||||||
|
let fullPath = (route.path || '').trim()
|
||||||
|
if (parentPath) {
|
||||||
|
if (fullPath.startsWith('/')) {
|
||||||
|
// 已是绝对路径,直接使用
|
||||||
|
} else {
|
||||||
|
const base = parentPath.endsWith('/') ? parentPath.slice(0, -1) : parentPath
|
||||||
|
fullPath = `${base}/${fullPath}`.replace(/\/+/g, '/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fullPath && !fullPath.startsWith('/')) {
|
||||||
|
fullPath = '/' + fullPath
|
||||||
|
}
|
||||||
|
if (route.children && route.children.length > 0) {
|
||||||
|
result.push(...this.flattenRoutes(route.children, fullPath))
|
||||||
|
} else if (route.meta && route.meta.title && fullPath) {
|
||||||
|
result.push({
|
||||||
|
path: fullPath,
|
||||||
|
label: route.meta.title,
|
||||||
|
icon: route.meta.icon || 'el-icon-menu',
|
||||||
|
iconClass: route.meta.icon,
|
||||||
|
route
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
},
|
||||||
isActive(path) {
|
isActive(path) {
|
||||||
if (!path) return false
|
if (!path) return false
|
||||||
const currentPath = this.$route.path
|
const currentPath = this.$route.path
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export default {
|
|||||||
pageSizes: {
|
pageSizes: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default() {
|
default() {
|
||||||
return [50, 100, 200, 500, 1000]
|
return [10, 50, 100, 200, 500, 1000]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 移动端页码按钮的数量端默认值5
|
// 移动端页码按钮的数量端默认值5
|
||||||
|
|||||||
@@ -10,9 +10,10 @@
|
|||||||
<app-main/>
|
<app-main/>
|
||||||
<settings ref="settingRef"/>
|
<settings ref="settingRef"/>
|
||||||
</div>
|
</div>
|
||||||
<!-- 移动端底部导航 -->
|
<!-- 移动端底部导航:key 随接口路由更新,确保菜单与跳转使用最新路由 -->
|
||||||
<mobile-bottom-nav
|
<mobile-bottom-nav
|
||||||
v-if="device === 'mobile'"
|
v-if="device === 'mobile'"
|
||||||
|
:key="mobileNavKey"
|
||||||
:items="mobileNavItems"
|
:items="mobileNavItems"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,6 +47,13 @@ export default {
|
|||||||
fixedHeader: state => state.settings.fixedHeader
|
fixedHeader: state => state.settings.fixedHeader
|
||||||
}),
|
}),
|
||||||
...mapGetters(['sidebarRouters']),
|
...mapGetters(['sidebarRouters']),
|
||||||
|
/** 接口路由更新后变化,使底部导航重新渲染并拉取最新菜单,避免点击跳错页 */
|
||||||
|
mobileNavKey() {
|
||||||
|
const routes = this.sidebarRouters || []
|
||||||
|
const len = routes.length
|
||||||
|
const firstPath = (len && routes[0]) ? (routes[0].path || '') : ''
|
||||||
|
return `${len}-${firstPath}`
|
||||||
|
},
|
||||||
mobileNavItems() {
|
mobileNavItems() {
|
||||||
// 如果返回空数组,组件会使用默认逻辑从路由中自动获取所有可用路由
|
// 如果返回空数组,组件会使用默认逻辑从路由中自动获取所有可用路由
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -154,39 +154,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 同步进度 -->
|
<!-- 表格行数(从接口获取,用于决定同步范围) -->
|
||||||
<el-card v-if="config.progressHint || config.currentProgress" class="progress-card-wrapper">
|
<el-card v-if="config.progressHint || config.currentProgress" class="progress-card-wrapper">
|
||||||
<div slot="header" class="card-header">
|
<div slot="header" class="card-header">
|
||||||
<i class="el-icon-data-line"></i>
|
<i class="el-icon-data-line"></i>
|
||||||
<span>同步进度</span>
|
<span>表格行数</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-content">
|
<div class="progress-content">
|
||||||
<div v-if="config.currentProgress" class="progress-detail">
|
<div v-if="config.currentProgress" class="progress-detail">
|
||||||
<div class="progress-item">
|
<div class="progress-item">
|
||||||
<span class="label">当前进度</span>
|
<span class="label">当前有数据行数</span>
|
||||||
<span class="value">第 {{ config.currentProgress }} 行</span>
|
<span class="value">第 {{ config.currentProgress }} 行(接口获取)</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-item">
|
<div class="progress-item">
|
||||||
<span class="label">下次同步</span>
|
<span class="label">下次同步起始</span>
|
||||||
<span class="value">
|
<span class="value">第 {{ config.nextStartRow != null ? config.nextStartRow : form.startRow }} 行</span>
|
||||||
<template v-if="config.currentProgress <= (form.startRow + 49)">
|
|
||||||
第 {{ form.startRow }} 行
|
|
||||||
</template>
|
|
||||||
<template v-else-if="config.currentProgress > (form.startRow + 100)">
|
|
||||||
第 {{ config.currentProgress - 100 }} 行
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
第 {{ form.startRow }} 行
|
|
||||||
</template>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-hint">
|
<div class="progress-hint">
|
||||||
<i class="el-icon-info"></i>
|
<i class="el-icon-info"></i>
|
||||||
系统自动回溯检查,防止遗漏
|
由接口实时获取表格行数,不再使用本地保存的进度
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="no-progress">
|
<div v-else class="no-progress">
|
||||||
{{ config.progressHint || '暂无同步进度' }}
|
{{ config.progressHint || '暂无表格行数(请先授权并配置)' }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|||||||
@@ -251,6 +251,56 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
|
||||||
|
<!-- Ollama 服务健康度(调试用) -->
|
||||||
|
<el-col :span="12" class="card-box">
|
||||||
|
<el-card>
|
||||||
|
<div slot="header" class="clearfix">
|
||||||
|
<span><i class="el-icon-cpu"></i> Ollama 服务健康度(调试)</span>
|
||||||
|
<el-button
|
||||||
|
style="float: right; padding: 3px 10px;"
|
||||||
|
type="primary"
|
||||||
|
size="mini"
|
||||||
|
:loading="ollamaTesting"
|
||||||
|
@click="testOllamaHealth"
|
||||||
|
>
|
||||||
|
{{ ollamaTesting ? '检测中...' : '测试' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="el-table el-table--enable-row-hover el-table--medium">
|
||||||
|
<table cellspacing="0" style="width: 100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="el-table__cell is-leaf"><div class="cell">服务状态</div></td>
|
||||||
|
<td class="el-table__cell is-leaf">
|
||||||
|
<div class="cell">
|
||||||
|
<el-tag :type="health.ollama && health.ollama.healthy ? 'success' : (health.ollama ? 'danger' : 'info')">
|
||||||
|
{{ health.ollama && health.ollama.status ? health.ollama.status : '未检测' }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="el-table__cell is-leaf"><div class="cell">服务地址</div></td>
|
||||||
|
<td class="el-table__cell is-leaf">
|
||||||
|
<div class="cell" style="word-break: break-all;">
|
||||||
|
{{ health.ollama && health.ollama.serviceUrl ? health.ollama.serviceUrl : '-' }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="el-table__cell is-leaf"><div class="cell">状态信息</div></td>
|
||||||
|
<td class="el-table__cell is-leaf">
|
||||||
|
<div class="cell" :class="{'text-danger': health.ollama && !health.ollama.healthy}">
|
||||||
|
{{ health.ollama && health.ollama.message ? health.ollama.message : '点击「测试」获取健康度' }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -267,8 +317,10 @@ export default {
|
|||||||
// 健康度检测信息
|
// 健康度检测信息
|
||||||
health: {
|
health: {
|
||||||
logistics: null,
|
logistics: null,
|
||||||
wxSend: null
|
wxSend: null,
|
||||||
}
|
ollama: null
|
||||||
|
},
|
||||||
|
ollamaTesting: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
@@ -294,6 +346,29 @@ export default {
|
|||||||
console.error("获取健康度检测信息失败", error)
|
console.error("获取健康度检测信息失败", error)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
/** 测试 Ollama 健康度(调试用) */
|
||||||
|
testOllamaHealth() {
|
||||||
|
this.ollamaTesting = true
|
||||||
|
getHealth()
|
||||||
|
.then(response => {
|
||||||
|
if (response.data) {
|
||||||
|
this.health = response.data
|
||||||
|
const ollama = response.data.ollama
|
||||||
|
if (ollama && ollama.healthy) {
|
||||||
|
this.$message.success('Ollama 服务正常')
|
||||||
|
} else {
|
||||||
|
this.$message.warning(ollama && ollama.message ? ollama.message : 'Ollama 服务异常或未配置')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Ollama 健康度检测失败', error)
|
||||||
|
this.$message.error('检测失败: ' + (error.message || '网络异常'))
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.ollamaTesting = false
|
||||||
|
})
|
||||||
|
},
|
||||||
// 打开加载层
|
// 打开加载层
|
||||||
openLoading() {
|
openLoading() {
|
||||||
this.$modal.loading("正在加载服务监控数据,请稍候!")
|
this.$modal.loading("正在加载服务监控数据,请稍候!")
|
||||||
|
|||||||
@@ -70,20 +70,34 @@
|
|||||||
<el-option label="500条" :value="500"></el-option>
|
<el-option label="500条" :value="500"></el-option>
|
||||||
<el-option label="1000条" :value="1000"></el-option>
|
<el-option label="1000条" :value="1000"></el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
|
<span class="history-label history-search-label">搜索匹配:</span>
|
||||||
|
<el-input
|
||||||
|
v-model="historySearchKeyword"
|
||||||
|
size="small"
|
||||||
|
placeholder="输入关键词,在全部历史数据中搜索"
|
||||||
|
clearable
|
||||||
|
style="width: 240px;"
|
||||||
|
prefix-icon="el-icon-search"
|
||||||
|
@input="onHistorySearchInput"
|
||||||
|
/>
|
||||||
|
<span v-if="historySearchKeyword.trim()" class="history-search-tip">(在全部数据中匹配)</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="history-container">
|
<div class="history-container">
|
||||||
<div class="history-column">
|
<div class="history-column">
|
||||||
<div class="history-header">
|
<div class="history-header">
|
||||||
<span>历史请求(最近 {{ historyLimit }} 条)</span>
|
<span>{{ historySearchKeyword.trim() ? `历史请求(关键词匹配 共 ${displayRequestList.length} 条)` : `历史请求(最近 ${historyLimit} 条)` }}</span>
|
||||||
<el-button size="mini" type="primary" icon="el-icon-refresh" @click="loadHistory" :loading="historyLoading">刷新</el-button>
|
<el-button size="mini" type="primary" icon="el-icon-refresh" @click="loadHistory" :loading="historyLoading">刷新</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="history-content">
|
<div class="history-content" v-loading="displayRequestLoading">
|
||||||
<div v-if="requestHistory.length === 0" class="empty-history">
|
<div v-if="!historySearchKeyword.trim() && requestHistory.length === 0" class="empty-history">
|
||||||
<el-empty description="暂无历史请求" :image-size="80" />
|
<el-empty description="暂无历史请求" :image-size="80" />
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="historySearchKeyword.trim() && displayRequestList.length === 0 && !displayRequestLoading" class="empty-history">
|
||||||
|
<el-empty description="无匹配结果" :image-size="80" />
|
||||||
|
</div>
|
||||||
<div v-else class="history-list">
|
<div v-else class="history-list">
|
||||||
<div v-for="(item, idx) in requestHistory" :key="'req-' + idx" class="history-item">
|
<div v-for="(item, idx) in displayRequestList" :key="'req-' + idx" class="history-item">
|
||||||
<div class="history-item-header">
|
<div class="history-item-header">
|
||||||
<div class="history-time">{{ extractTime(item) }}</div>
|
<div class="history-time">{{ extractTime(item) }}</div>
|
||||||
<el-button
|
<el-button
|
||||||
@@ -102,14 +116,17 @@
|
|||||||
|
|
||||||
<div class="history-column">
|
<div class="history-column">
|
||||||
<div class="history-header">
|
<div class="history-header">
|
||||||
<span>历史响应(最近 {{ historyLimit }} 条)</span>
|
<span>{{ historySearchKeyword.trim() ? `历史响应(关键词匹配 共 ${displayResponseList.length} 条)` : `历史响应(最近 ${historyLimit} 条)` }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="history-content">
|
<div class="history-content" v-loading="displayResponseLoading">
|
||||||
<div v-if="responseHistory.length === 0" class="empty-history">
|
<div v-if="!historySearchKeyword.trim() && responseHistory.length === 0" class="empty-history">
|
||||||
<el-empty description="暂无历史响应" :image-size="80" />
|
<el-empty description="暂无历史响应" :image-size="80" />
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="historySearchKeyword.trim() && displayResponseList.length === 0 && !displayResponseLoading" class="empty-history">
|
||||||
|
<el-empty description="无匹配结果" :image-size="80" />
|
||||||
|
</div>
|
||||||
<div v-else class="history-list">
|
<div v-else class="history-list">
|
||||||
<div v-for="(item, idx) in responseHistory" :key="'res-' + idx" class="history-item">
|
<div v-for="(item, idx) in displayResponseList" :key="'res-' + idx" class="history-item">
|
||||||
<div class="history-item-header">
|
<div class="history-item-header">
|
||||||
|
|
||||||
<div class="history-time">{{ extractTime(item) }}</div>
|
<div class="history-time">{{ extractTime(item) }}</div>
|
||||||
@@ -178,6 +195,11 @@ export default {
|
|||||||
responseHistory: [],
|
responseHistory: [],
|
||||||
historyLoading: false,
|
historyLoading: false,
|
||||||
historyLimit: 50,
|
historyLimit: 50,
|
||||||
|
historySearchKeyword: '', // 历史记录搜索关键词(有值时请求后端在全部数据中搜索)
|
||||||
|
searchRequestList: [], // 服务端返回的请求搜索结果
|
||||||
|
searchResponseList: [], // 服务端返回的响应搜索结果
|
||||||
|
historySearchLoading: false,
|
||||||
|
historySearchTimer: null, // 防抖定时器
|
||||||
// 验证码相关
|
// 验证码相关
|
||||||
verifyDialogVisible: false,
|
verifyDialogVisible: false,
|
||||||
verifyCode: '',
|
verifyCode: '',
|
||||||
@@ -188,6 +210,19 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
// 当前展示的请求列表:有搜索关键词时用服务端搜索结果,否则用本地已加载的 requestHistory
|
||||||
|
displayRequestList() {
|
||||||
|
return this.historySearchKeyword.trim() ? this.searchRequestList : this.requestHistory
|
||||||
|
},
|
||||||
|
displayResponseList() {
|
||||||
|
return this.historySearchKeyword.trim() ? this.searchResponseList : this.responseHistory
|
||||||
|
},
|
||||||
|
displayRequestLoading() {
|
||||||
|
return !!this.historySearchKeyword.trim() && this.historySearchLoading
|
||||||
|
},
|
||||||
|
displayResponseLoading() {
|
||||||
|
return !!this.historySearchKeyword.trim() && this.historySearchLoading
|
||||||
|
},
|
||||||
// 生成完整消息,用三个空行分隔
|
// 生成完整消息,用三个空行分隔
|
||||||
fullMessage() {
|
fullMessage() {
|
||||||
if (!this.resultList || this.resultList.length === 0) return ''
|
if (!this.resultList || this.resultList.length === 0) return ''
|
||||||
@@ -390,6 +425,48 @@ export default {
|
|||||||
this.$modal.msgError('加载历史记录失败')
|
this.$modal.msgError('加载历史记录失败')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
// 搜索框输入:防抖后请求后端在全部历史数据中搜索
|
||||||
|
onHistorySearchInput() {
|
||||||
|
if (this.historySearchTimer) clearTimeout(this.historySearchTimer)
|
||||||
|
const kw = (this.historySearchKeyword || '').trim()
|
||||||
|
if (!kw) {
|
||||||
|
this.searchRequestList = []
|
||||||
|
this.searchResponseList = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.historySearchTimer = setTimeout(() => {
|
||||||
|
this.loadHistorySearch(kw)
|
||||||
|
}, 300)
|
||||||
|
},
|
||||||
|
// 按关键词在全部数据中搜索(后端接口)
|
||||||
|
loadHistorySearch(keyword) {
|
||||||
|
if (!keyword || !keyword.trim()) {
|
||||||
|
this.searchRequestList = []
|
||||||
|
this.searchResponseList = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.historySearchLoading = true
|
||||||
|
const limit = 500
|
||||||
|
Promise.all([
|
||||||
|
getHistory('request', limit, keyword),
|
||||||
|
getHistory('response', limit, keyword)
|
||||||
|
]).then(([reqRes, respRes]) => {
|
||||||
|
this.historySearchLoading = false
|
||||||
|
if (reqRes && reqRes.code === 200) {
|
||||||
|
this.searchRequestList = reqRes.data || []
|
||||||
|
} else {
|
||||||
|
this.searchRequestList = []
|
||||||
|
}
|
||||||
|
if (respRes && respRes.code === 200) {
|
||||||
|
this.searchResponseList = respRes.data || []
|
||||||
|
} else {
|
||||||
|
this.searchResponseList = []
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
this.historySearchLoading = false
|
||||||
|
this.$modal.msgError('搜索历史记录失败')
|
||||||
|
})
|
||||||
|
},
|
||||||
extractTime(item) {
|
extractTime(item) {
|
||||||
if (!item) return ''
|
if (!item) return ''
|
||||||
const idx = item.indexOf(' | ')
|
const idx = item.indexOf(' | ')
|
||||||
@@ -811,6 +888,16 @@ export default {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.history-search-label {
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-search-tip {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 历史消息容器 */
|
/* 历史消息容器 */
|
||||||
.history-container {
|
.history-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -135,39 +135,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 同步进度 -->
|
<!-- 表格行数(从接口获取,用于决定同步范围) -->
|
||||||
<div v-if="config.progressHint" class="progress-card">
|
<div v-if="config.progressHint || config.currentProgress" class="progress-card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<i class="el-icon-data-line"></i>
|
<i class="el-icon-data-line"></i>
|
||||||
<span>同步进度</span>
|
<span>表格行数</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-content">
|
<div class="progress-content">
|
||||||
<div v-if="config.currentProgress" class="progress-detail">
|
<div v-if="config.currentProgress" class="progress-detail">
|
||||||
<div class="progress-item">
|
<div class="progress-item">
|
||||||
<span class="label">当前进度</span>
|
<span class="label">当前有数据行数</span>
|
||||||
<span class="value">第 {{ config.currentProgress }} 行</span>
|
<span class="value">第 {{ config.currentProgress }} 行(接口获取)</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-item">
|
<div class="progress-item">
|
||||||
<span class="label">下次同步</span>
|
<span class="label">下次同步起始</span>
|
||||||
<span class="value">
|
<span class="value">第 {{ config.nextStartRow != null ? config.nextStartRow : form.startRow }} 行</span>
|
||||||
<template v-if="config.currentProgress <= (form.startRow + 49)">
|
|
||||||
第 {{ form.startRow }} 行
|
|
||||||
</template>
|
|
||||||
<template v-else-if="config.currentProgress > (form.startRow + 100)">
|
|
||||||
第 {{ config.currentProgress - 100 }} 行
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
第 {{ form.startRow }} 行
|
|
||||||
</template>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-hint">
|
<div class="progress-hint">
|
||||||
<i class="el-icon-info"></i>
|
<i class="el-icon-info"></i>
|
||||||
系统自动回溯检查,防止遗漏
|
由接口实时获取表格行数,不再使用本地保存的进度
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="no-progress">
|
<div v-else class="no-progress">
|
||||||
{{ config.progressHint }}
|
{{ config.progressHint || '暂无表格行数(请先授权并配置)' }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -117,6 +117,7 @@
|
|||||||
<el-button type="warning" size="small" icon="el-icon-sort" @click="handleReverseSyncThirdPartyOrderNo" :loading="reverseSyncLoading" title="从腾讯文档第850行开始,通过物流链接反向匹配订单,将腾讯文档的单号列值写入到订单的第三方单号字段">反向同步第三方单号</el-button>
|
<el-button type="warning" size="small" icon="el-icon-sort" @click="handleReverseSyncThirdPartyOrderNo" :loading="reverseSyncLoading" title="从腾讯文档第850行开始,通过物流链接反向匹配订单,将腾讯文档的单号列值写入到订单的第三方单号字段">反向同步第三方单号</el-button>
|
||||||
<el-button v-if="!isMobile" type="primary" size="small" icon="el-icon-document-copy" @click="handleBatchCopyExcelText" :disabled="selectedRows.length === 0" title="批量复制选中订单的录单格式(Excel可粘贴)">批量复制录单格式</el-button>
|
<el-button v-if="!isMobile" type="primary" size="small" icon="el-icon-document-copy" @click="handleBatchCopyExcelText" :disabled="selectedRows.length === 0" title="批量复制选中订单的录单格式(Excel可粘贴)">批量复制录单格式</el-button>
|
||||||
<el-button v-if="!isMobile" type="success" size="small" icon="el-icon-document-copy" @click="handleBatchCopyRebateText" :disabled="selectedRows.length === 0" title="批量复制选中订单的后返录表格式(Excel可粘贴)">批量复制后返录表</el-button>
|
<el-button v-if="!isMobile" type="success" size="small" icon="el-icon-document-copy" @click="handleBatchCopyRebateText" :disabled="selectedRows.length === 0" title="批量复制选中订单的后返录表格式(Excel可粘贴)">批量复制后返录表</el-button>
|
||||||
|
<el-button v-if="!isMobile" type="info" size="small" icon="el-icon-document-copy" @click="handleBatchCopySichuanCommerceText" :disabled="selectedRows.length === 0" title="批量复制选中订单的四川商贸录表格式(日期 型号 数量 地址 价格 备注 是否安排 物流)">四川商贸录表</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -1506,8 +1507,8 @@ export default {
|
|||||||
confirmMsg += `数据起始行: ${config.startRow}\n`
|
confirmMsg += `数据起始行: ${config.startRow}\n`
|
||||||
|
|
||||||
if (config.currentProgress) {
|
if (config.currentProgress) {
|
||||||
confirmMsg += `\n当前进度: 第 ${config.currentProgress} 行\n`
|
confirmMsg += `\n表格行数: 第 ${config.currentProgress} 行(接口获取)\n`
|
||||||
confirmMsg += config.progressHint + '\n'
|
confirmMsg += (config.progressHint || '') + '\n'
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmMsg += `\n防重推送: ${config.skipPushedOrders ? '已启用(跳过已推送订单)' : '已禁用'}\n`
|
confirmMsg += `\n防重推送: ${config.skipPushedOrders ? '已启用(跳过已推送订单)' : '已禁用'}\n`
|
||||||
@@ -2309,6 +2310,49 @@ export default {
|
|||||||
this.$message.error('批量复制失败:' + (e.message || '未知错误'))
|
this.$message.error('批量复制失败:' + (e.message || '未知错误'))
|
||||||
console.error('批量复制后返录表格式失败', e)
|
console.error('批量复制后返录表格式失败', e)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 批量复制四川商贸录表(格式:日期 型号 数量 地址 价格 备注 是否安排 物流) */
|
||||||
|
handleBatchCopySichuanCommerceText() {
|
||||||
|
if (!this.selectedRows || this.selectedRows.length === 0) {
|
||||||
|
this.$message.warning('请先选择要复制的订单')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const header = ['日期', '型号', '数量', '地址', '价格', '备注', '是否安排', '物流'].join('\t')
|
||||||
|
const lines = [header]
|
||||||
|
|
||||||
|
this.selectedRows.forEach(row => {
|
||||||
|
// 日期(格式:yyyy/MM/dd)
|
||||||
|
let dateStr = ''
|
||||||
|
if (row.orderTime) {
|
||||||
|
const date = new Date(row.orderTime)
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
dateStr = `${year}/${month}/${day}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelNumber = row.modelNumber || ''
|
||||||
|
const quantity = (row.productCount != null && row.productCount !== '') ? String(row.productCount) : '1'
|
||||||
|
const address = row.address || ''
|
||||||
|
const priceStr = row.paymentAmount != null ? row.paymentAmount.toFixed(2) : ''
|
||||||
|
const remark = row.remark || ''
|
||||||
|
const arranged = '' // 是否安排,留空由用户填写
|
||||||
|
const logistics = row.logisticsLink || ''
|
||||||
|
|
||||||
|
const text = [dateStr, modelNumber, quantity, address, priceStr, remark, arranged, logistics].join('\t')
|
||||||
|
lines.push(text)
|
||||||
|
})
|
||||||
|
|
||||||
|
const finalText = lines.join('\n')
|
||||||
|
this.copyToClipboard(finalText)
|
||||||
|
this.$message.success(`已复制 ${this.selectedRows.length} 条订单的四川商贸录表格式到剪贴板(含表头),可直接粘贴到Excel`)
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error('批量复制失败:' + (e.message || '未知错误'))
|
||||||
|
console.error('批量复制四川商贸录表失败', e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -318,7 +318,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { listOrderrows, getOrderrows, delOrderrows, addOrderrows, updateOrderrows, getValidCodeSelectData, getOrderStatistics } from "@/api/system/orderrows";
|
import { listOrderrows, getOrderrows, delOrderrows, addOrderrows, updateOrderrows, getValidCodeSelectData } from "@/api/system/orderrows";
|
||||||
import { getAdminSelectData } from "@/api/system/superadmin";
|
import { getAdminSelectData } from "@/api/system/superadmin";
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
import MobileSearchForm from '@/components/MobileSearchForm'
|
import MobileSearchForm from '@/components/MobileSearchForm'
|
||||||
@@ -436,31 +436,25 @@ export default {
|
|||||||
this.orderrowsList = response.rows;
|
this.orderrowsList = response.rows;
|
||||||
this.total = response.total;
|
this.total = response.total;
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
// 调用后端统计接口计算统计数据
|
// 统计已合并到列表接口返回的 statistics,与列表同条件
|
||||||
this.loadStatistics();
|
this.applyStatistics(response.statistics);
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error('获取订单列表失败:', error);
|
console.error('获取订单列表失败:', error);
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.$message.error('获取订单列表失败');
|
this.$message.error('获取订单列表失败');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
/** 加载统计数据(从后端获取) */
|
/** 使用列表接口返回的 statistics 填充统计(与列表同一次请求、同条件) */
|
||||||
loadStatistics() {
|
applyStatistics(data) {
|
||||||
// 构建统计查询参数,使用与列表查询相同的条件
|
if (!data) {
|
||||||
const statParams = {
|
if (this.orderrowsList.length > 0) this.calculateStatistics();
|
||||||
...this.queryParams,
|
else this.statistics = { totalOrders: 0, totalCosPrice: 0, totalEstimateFee: 0, totalActualFee: 0, statusStats: {}, accountStats: {} };
|
||||||
beginTime: this.dateRange && this.dateRange.length > 0 ? this.dateRange[0] : null,
|
return;
|
||||||
endTime: this.dateRange && this.dateRange.length > 1 ? this.dateRange[1] : null
|
}
|
||||||
};
|
|
||||||
|
|
||||||
getOrderStatistics(statParams).then(response => {
|
|
||||||
const data = response.data || {};
|
|
||||||
const groupStats = data.groupStats || {};
|
const groupStats = data.groupStats || {};
|
||||||
|
|
||||||
// 转换后端返回的统计格式为前端需要的格式
|
|
||||||
this.statistics = {
|
this.statistics = {
|
||||||
totalOrders: data.totalOrders || 0,
|
totalOrders: data.totalOrders || 0,
|
||||||
totalCosPrice: 0, // 后端没有返回此字段,如果需要可以前端计算或后端添加
|
totalCosPrice: data.totalCosPrice != null ? data.totalCosPrice : 0,
|
||||||
totalEstimateFee: data.totalCommission || 0,
|
totalEstimateFee: data.totalCommission || 0,
|
||||||
totalActualFee: data.totalActualFee || 0,
|
totalActualFee: data.totalActualFee || 0,
|
||||||
statusStats: {
|
statusStats: {
|
||||||
@@ -472,68 +466,35 @@ export default {
|
|||||||
deposit: this.convertGroupStat(groupStats.deposit),
|
deposit: this.convertGroupStat(groupStats.deposit),
|
||||||
illegal: this.convertGroupStat(groupStats.illegal)
|
illegal: this.convertGroupStat(groupStats.illegal)
|
||||||
},
|
},
|
||||||
accountStats: {} // 后端没有按账号统计,保留为空或后续添加
|
accountStats: {}
|
||||||
};
|
};
|
||||||
|
// 按账号统计仅用当前页列表轻量汇总
|
||||||
// 如果有订单列表,计算总计佣金额和按账号统计(这些前端计算更快)
|
|
||||||
if (this.orderrowsList.length > 0) {
|
if (this.orderrowsList.length > 0) {
|
||||||
let totalCosPrice = 0;
|
|
||||||
const accountStats = {};
|
const accountStats = {};
|
||||||
|
|
||||||
this.orderrowsList.forEach(order => {
|
this.orderrowsList.forEach(order => {
|
||||||
// 总计佣金额
|
|
||||||
if (order.estimateCosPrice) {
|
|
||||||
totalCosPrice += parseFloat(order.estimateCosPrice) || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按账号统计
|
|
||||||
const unionId = order.unionId;
|
const unionId = order.unionId;
|
||||||
if (!accountStats[unionId]) {
|
if (!accountStats[unionId]) accountStats[unionId] = { count: 0, amount: 0 };
|
||||||
accountStats[unionId] = {
|
|
||||||
count: 0,
|
|
||||||
amount: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
accountStats[unionId].count++;
|
accountStats[unionId].count++;
|
||||||
|
|
||||||
// 计算账号佣金金额(使用与后端相同的逻辑)
|
|
||||||
const validCode = String(order.validCode);
|
const validCode = String(order.validCode);
|
||||||
const isCancel = validCode === '3';
|
const isCancel = validCode === '3';
|
||||||
const isIllegal = ['25', '26', '27', '28'].includes(validCode);
|
const isIllegal = ['25', '26', '27', '28'].includes(validCode);
|
||||||
|
|
||||||
let commissionAmount = parseFloat(order.actualFee) || 0;
|
let commissionAmount = parseFloat(order.actualFee) || 0;
|
||||||
if (isIllegal && order.estimateCosPrice && order.commissionRate) {
|
if (isIllegal && order.estimateCosPrice && order.commissionRate) {
|
||||||
commissionAmount = parseFloat(order.estimateCosPrice) * parseFloat(order.commissionRate) / 100;
|
commissionAmount = parseFloat(order.estimateCosPrice) * parseFloat(order.commissionRate) / 100;
|
||||||
} else if (isCancel && (!order.actualFee || parseFloat(order.actualFee) === 0)
|
} else if (isCancel && (!order.actualFee || parseFloat(order.actualFee) === 0) && order.estimateCosPrice && order.commissionRate) {
|
||||||
&& order.estimateCosPrice && order.commissionRate) {
|
|
||||||
commissionAmount = parseFloat(order.estimateCosPrice) * parseFloat(order.commissionRate) / 100;
|
commissionAmount = parseFloat(order.estimateCosPrice) * parseFloat(order.commissionRate) / 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
accountStats[unionId].amount += commissionAmount;
|
accountStats[unionId].amount += commissionAmount;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.statistics.totalCosPrice = totalCosPrice;
|
|
||||||
this.statistics.accountStats = accountStats;
|
this.statistics.accountStats = accountStats;
|
||||||
}
|
}
|
||||||
}).catch(error => {
|
|
||||||
console.error('获取统计数据失败:', error);
|
|
||||||
// 如果后端统计失败,回退到前端计算
|
|
||||||
this.calculateStatistics();
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
/** 转换后端分组统计格式 */
|
|
||||||
convertGroupStat(groupStat) {
|
convertGroupStat(groupStat) {
|
||||||
if (!groupStat) {
|
if (!groupStat) return { label: '', count: 0, amount: 0 };
|
||||||
return {
|
|
||||||
label: '',
|
|
||||||
count: 0,
|
|
||||||
amount: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
label: groupStat.label || '',
|
label: groupStat.label || '',
|
||||||
count: groupStat.count || 0,
|
count: groupStat.count || 0,
|
||||||
amount: groupStat.actualFee || 0 // 使用actualFee作为金额
|
amount: groupStat.actualFee ?? 0
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
/** 获取管理员列表 */
|
/** 获取管理员列表 */
|
||||||
|
|||||||
364
src/views/system/social-media/xianyu-wenan.vue
Normal file
364
src/views/system/social-media/xianyu-wenan.vue
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
<template>
|
||||||
|
<div class="xianyu-wenan-container">
|
||||||
|
<el-card class="box-card">
|
||||||
|
<div slot="header" class="clearfix">
|
||||||
|
<span class="card-title">
|
||||||
|
<i class="el-icon-edit-outline"></i>
|
||||||
|
闲鱼文案(手动)
|
||||||
|
</span>
|
||||||
|
<el-button
|
||||||
|
style="float: right; padding: 3px 0"
|
||||||
|
type="text"
|
||||||
|
@click="showHelp = !showHelp">
|
||||||
|
{{ showHelp ? '隐藏帮助' : '显示帮助' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-collapse-transition>
|
||||||
|
<div v-show="showHelp" class="help-section">
|
||||||
|
<el-alert
|
||||||
|
title="使用说明"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon>
|
||||||
|
<div slot="default">
|
||||||
|
<p><strong>功能说明:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>不依赖 JD 接口,手动输入标题即可生成闲鱼文案,用于接口限流时的应急</li>
|
||||||
|
<li>一键生成「代下单」和「教你下单」两种文案</li>
|
||||||
|
<li>标题和型号会自动清洗敏感词(以旧、政府、换新等)</li>
|
||||||
|
<li>支持可选备注(型号),会拼在标题后参与生成</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</el-alert>
|
||||||
|
</div>
|
||||||
|
</el-collapse-transition>
|
||||||
|
|
||||||
|
<div class="input-section">
|
||||||
|
<el-form
|
||||||
|
:model="form"
|
||||||
|
:label-width="labelWidth"
|
||||||
|
:label-position="labelPosition"
|
||||||
|
class="main-form">
|
||||||
|
<el-form-item label="商品标题" required>
|
||||||
|
<el-input
|
||||||
|
v-model="form.title"
|
||||||
|
type="textarea"
|
||||||
|
:rows="mobile ? 3 : 2"
|
||||||
|
placeholder="请输入商品标题(必填)"
|
||||||
|
clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="型号/备注">
|
||||||
|
<el-input
|
||||||
|
v-model="form.remark"
|
||||||
|
placeholder="选填,如型号、规格等,会拼在标题后"
|
||||||
|
clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
class="btn-generate"
|
||||||
|
:loading="generating"
|
||||||
|
@click="handleGenerate">
|
||||||
|
生成闲鱼文案
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
class="btn-clear"
|
||||||
|
:disabled="!form.title && !form.remark"
|
||||||
|
@click="handleClearInput">
|
||||||
|
<i class="el-icon-delete"></i> 清空输入
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="result.daixiadan || result.jiaonixiadan" class="result-section">
|
||||||
|
<el-row :gutter="gutter">
|
||||||
|
<el-col :xs="24" :sm="24" :md="12">
|
||||||
|
<el-card shadow="hover" class="result-card">
|
||||||
|
<div slot="header" class="result-card-header">
|
||||||
|
<span class="result-card-title">代下单(一键代下)</span>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
class="btn-copy"
|
||||||
|
@click="copyResult('daixiadan')">
|
||||||
|
<i class="el-icon-document-copy"></i> 复制
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="result-content">{{ result.daixiadan }}</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="24" :md="12">
|
||||||
|
<el-card shadow="hover" class="result-card">
|
||||||
|
<div slot="header" class="result-card-header">
|
||||||
|
<span class="result-card-title">教你下单</span>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
class="btn-copy"
|
||||||
|
@click="copyResult('jiaonixiadan')">
|
||||||
|
<i class="el-icon-document-copy"></i> 复制
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="result-content">{{ result.jiaonixiadan }}</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { generateXianyuWenan } from '@/api/jarvis/socialMedia'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'XianyuWenan',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showHelp: false,
|
||||||
|
form: {
|
||||||
|
title: '',
|
||||||
|
remark: ''
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
daixiadan: '',
|
||||||
|
jiaonixiadan: ''
|
||||||
|
},
|
||||||
|
generating: false,
|
||||||
|
mobile: false,
|
||||||
|
resizeTimer: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
labelWidth() {
|
||||||
|
return this.mobile ? '80px' : '100px'
|
||||||
|
},
|
||||||
|
labelPosition() {
|
||||||
|
return this.mobile ? 'top' : 'right'
|
||||||
|
},
|
||||||
|
gutter() {
|
||||||
|
return this.mobile ? 12 : 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.checkMobile()
|
||||||
|
window.addEventListener('resize', this.onResize)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener('resize', this.onResize)
|
||||||
|
if (this.resizeTimer) clearTimeout(this.resizeTimer)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
checkMobile() {
|
||||||
|
this.mobile = document.documentElement.clientWidth < 768
|
||||||
|
},
|
||||||
|
onResize() {
|
||||||
|
if (this.resizeTimer) clearTimeout(this.resizeTimer)
|
||||||
|
this.resizeTimer = setTimeout(this.checkMobile, 150)
|
||||||
|
},
|
||||||
|
handleClearInput() {
|
||||||
|
this.form.title = ''
|
||||||
|
this.form.remark = ''
|
||||||
|
this.$message.success('已清空输入')
|
||||||
|
},
|
||||||
|
async handleGenerate() {
|
||||||
|
const title = (this.form.title || '').trim()
|
||||||
|
if (!title) {
|
||||||
|
this.$message.warning('请输入商品标题')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.generating = true
|
||||||
|
try {
|
||||||
|
const res = await generateXianyuWenan({
|
||||||
|
title: title,
|
||||||
|
remark: (this.form.remark || '').trim() || undefined
|
||||||
|
})
|
||||||
|
if (res.code === 200 && res.data) {
|
||||||
|
const data = res.data
|
||||||
|
if (data.success) {
|
||||||
|
this.result.daixiadan = data.daixiadan || ''
|
||||||
|
this.result.jiaonixiadan = data.jiaonixiadan || ''
|
||||||
|
this.$message.success('生成成功')
|
||||||
|
} else {
|
||||||
|
this.$message.error(data.error || '生成失败')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '生成失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('生成失败', error)
|
||||||
|
this.$message.error('生成失败:' + (error.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
this.generating = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
copyResult(type) {
|
||||||
|
const text = this.result[type]
|
||||||
|
if (!text) {
|
||||||
|
this.$message.warning('暂无内容可复制')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
this.$message.success('已复制到剪贴板')
|
||||||
|
}).catch(() => {
|
||||||
|
this.fallbackCopy(text)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.fallbackCopy(text)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fallbackCopy(text) {
|
||||||
|
const ta = document.createElement('textarea')
|
||||||
|
ta.value = text
|
||||||
|
ta.style.position = 'fixed'
|
||||||
|
ta.style.left = '-9999px'
|
||||||
|
document.body.appendChild(ta)
|
||||||
|
ta.select()
|
||||||
|
try {
|
||||||
|
document.execCommand('copy')
|
||||||
|
this.$message.success('已复制到剪贴板')
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error('复制失败,请手动选择复制')
|
||||||
|
}
|
||||||
|
document.body.removeChild(ta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.xianyu-wenan-container {
|
||||||
|
padding: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.box-card {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.card-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.help-section {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.input-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.result-section {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.result-card {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.result-card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.result-card-title {
|
||||||
|
font-weight: 500;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.btn-copy {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.result-content {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
.btn-clear {
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端适配 */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.xianyu-wenan-container {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
.box-card {
|
||||||
|
margin: 0 -8px;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.box-card ::v-deep .el-card__header {
|
||||||
|
padding: 12px 15px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.box-card ::v-deep .el-card__body {
|
||||||
|
padding: 12px 15px;
|
||||||
|
}
|
||||||
|
.card-title {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.help-section {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.help-section ::v-deep .el-alert__content {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.input-section {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.main-form ::v-deep .el-form-item {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.main-form ::v-deep .el-form-item__label {
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
.btn-generate {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
height: 44px;
|
||||||
|
font-size: 15px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.btn-clear {
|
||||||
|
width: 100%;
|
||||||
|
height: 44px;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
.result-section {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
.result-section ::v-deep .el-row {
|
||||||
|
margin-left: -6px !important;
|
||||||
|
margin-right: -6px !important;
|
||||||
|
}
|
||||||
|
.result-section ::v-deep .el-col {
|
||||||
|
padding-left: 6px !important;
|
||||||
|
padding-right: 6px !important;
|
||||||
|
}
|
||||||
|
.result-card {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.result-card ::v-deep .el-card__header {
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
.result-card ::v-deep .el-card__body {
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
.result-card-title {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.btn-copy {
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
.result-content {
|
||||||
|
font-size: 13px;
|
||||||
|
max-height: 320px;
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user