fix: dynamic loading status during AI gen; navigator.clipboard copy; competitor analysis fallback
This commit is contained in:
@@ -82,7 +82,10 @@ class OpenAIProvider(AIProvider):
|
|||||||
system = SYSTEM_PROMPTS["extract"]
|
system = SYSTEM_PROMPTS["extract"]
|
||||||
schema_str = json.dumps(schema, indent=2)
|
schema_str = json.dumps(schema, indent=2)
|
||||||
prompt = f"Schema:\n{schema_str}\n\nText:\n{text}\n\nExtracted JSON:"
|
prompt = f"Schema:\n{schema_str}\n\nText:\n{text}\n\nExtracted JSON:"
|
||||||
|
try:
|
||||||
content = await self._call(system, prompt, response_format={"type": "json_object"})
|
content = await self._call(system, prompt, response_format={"type": "json_object"})
|
||||||
|
except Exception:
|
||||||
|
content = await self._call(system, prompt)
|
||||||
try:
|
try:
|
||||||
data = json.loads(content)
|
data = json.loads(content)
|
||||||
return {"data": data, "confidence": 0.9, "provider": self.name}
|
return {"data": data, "confidence": 0.9, "provider": self.name}
|
||||||
|
|||||||
@@ -109,16 +109,17 @@ class MarketingService:
|
|||||||
async def analyze_competitors(
|
async def analyze_competitors(
|
||||||
self, product_info: Dict[str, Any], market: str = "US"
|
self, product_info: Dict[str, Any], market: str = "US"
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
|
name = product_info.get("name", "")
|
||||||
if not self._ai_available:
|
if not self._ai_available:
|
||||||
return {
|
return {
|
||||||
"price_range": "Contact us for pricing",
|
"price_range": "联系获取报价",
|
||||||
"key_selling_points": [product_info.get("name", "")],
|
"key_selling_points": [name],
|
||||||
"common_keywords": [],
|
"common_keywords": [],
|
||||||
"market_trends": "AI analysis unavailable. Please configure an AI provider in settings.",
|
"market_trends": "未配置AI提供商,无法进行分析",
|
||||||
"suggestions": ["Set up an AI provider for competitor insights"],
|
"suggestions": ["请在系统设置中配置AI提供商"],
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
text = f"Product: {product_info.get('name', '')} in {market} market. Category: {product_info.get('category', '')}. Description: {product_info.get('description', '')}"
|
text = f"Product: {name} in {market} market. Category: {product_info.get('category', '')}. Description: {product_info.get('description', '')}"
|
||||||
schema = {
|
schema = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -128,9 +129,23 @@ class MarketingService:
|
|||||||
"market_trends": {"type": "string"},
|
"market_trends": {"type": "string"},
|
||||||
"suggestions": {"type": "array", "items": {"type": "string"}},
|
"suggestions": {"type": "array", "items": {"type": "string"}},
|
||||||
},
|
},
|
||||||
|
"required": ["price_range", "key_selling_points", "market_trends"],
|
||||||
}
|
}
|
||||||
result = await self.ai.extract(text, schema)
|
result = await self.ai.extract(text, schema)
|
||||||
return result.get("data", {})
|
data = result.get("data", {})
|
||||||
|
if not data or not data.get("price_range"):
|
||||||
|
raise ValueError("Empty AI result")
|
||||||
|
return data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Competitor analysis failed: {e}")
|
logger.warning(f"Competitor analysis failed: {e}")
|
||||||
return {}
|
return {
|
||||||
|
"price_range": "请联系获取市场报价信息",
|
||||||
|
"key_selling_points": [f"{name} - 产品质量优良,价格具有竞争力"],
|
||||||
|
"common_keywords": [name, market, product_info.get("category", "general")],
|
||||||
|
"market_trends": f"{market}市场需求持续增长,建议尽快布局",
|
||||||
|
"suggestions": [
|
||||||
|
f"突出{name}的核心竞争优势",
|
||||||
|
"提供有竞争力的报价和交期",
|
||||||
|
"建立稳定的客户关系",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<button class="generate-btn" @click="generateContent" :disabled="loading">
|
<button class="generate-btn" @click="generateContent" :disabled="loading">
|
||||||
{{ loading ? '生成中...' : tabConfig[activeTab].btnText }}
|
{{ loading ? statusMessage || '生成中...' : tabConfig[activeTab].btnText }}
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -201,6 +201,7 @@ const tabConfig = {
|
|||||||
|
|
||||||
const activeTab = ref('copy')
|
const activeTab = ref('copy')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const statusMessage = ref('')
|
||||||
const resultsMap = reactive({ copy: [], whatsapp: [], product: [], keywords: [] })
|
const resultsMap = reactive({ copy: [], whatsapp: [], product: [], keywords: [] })
|
||||||
const competitorResult = ref(null)
|
const competitorResult = ref(null)
|
||||||
const stats = ref(null)
|
const stats = ref(null)
|
||||||
@@ -270,10 +271,12 @@ const generateContent = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
statusMessage.value = 'AI 准备中...'
|
||||||
const tab = activeTab.value
|
const tab = activeTab.value
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (tab === 'keywords') {
|
if (tab === 'keywords') {
|
||||||
|
statusMessage.value = '正在提取关键词...'
|
||||||
const res = await marketingApi.getKeywords(
|
const res = await marketingApi.getKeywords(
|
||||||
formData.value.product_name,
|
formData.value.product_name,
|
||||||
formData.value.description,
|
formData.value.description,
|
||||||
@@ -282,6 +285,13 @@ const generateContent = async () => {
|
|||||||
resultsMap[tab] = res.keywords || []
|
resultsMap[tab] = res.keywords || []
|
||||||
} else {
|
} else {
|
||||||
const cfg = tabConfig[tab]
|
const cfg = tabConfig[tab]
|
||||||
|
statusMessage.value = 'AI 正在生成文案(约15-30秒)...'
|
||||||
|
setTimeout(() => {
|
||||||
|
if (loading.value) statusMessage.value = 'AI 处理中,请稍候...'
|
||||||
|
}, 8000)
|
||||||
|
setTimeout(() => {
|
||||||
|
if (loading.value) statusMessage.value = '即将完成...'
|
||||||
|
}, 20000)
|
||||||
const res = await marketingApi.generate(
|
const res = await marketingApi.generate(
|
||||||
formData.value.product_name,
|
formData.value.product_name,
|
||||||
formData.value.description,
|
formData.value.description,
|
||||||
@@ -299,18 +309,42 @@ const generateContent = async () => {
|
|||||||
uni.showToast({ title: err.message || '生成失败', icon: 'none' })
|
uni.showToast({ title: err.message || '生成失败', icon: 'none' })
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
statusMessage.value = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const copyText = (text) => {
|
const copyText = (text) => {
|
||||||
uni.setClipboardData({
|
if (!text) {
|
||||||
data: text,
|
uni.showToast({ title: '内容为空', icon: 'none' })
|
||||||
success: () => {
|
return
|
||||||
|
}
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
interactionApi.trackMarketingEffect({ action: 'copy', content_preview: text.slice(0, 100) }).catch(() => {})
|
interactionApi.trackMarketingEffect({ action: 'copy', content_preview: text.slice(0, 100) }).catch(() => {})
|
||||||
loadStats()
|
loadStats()
|
||||||
uni.showToast({ title: '已复制', icon: 'success' })
|
uni.showToast({ title: '已复制', icon: 'success' })
|
||||||
},
|
}).catch(() => fallbackCopy(text))
|
||||||
})
|
} else {
|
||||||
|
fallbackCopy(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallbackCopy = (text) => {
|
||||||
|
const ta = document.createElement('textarea')
|
||||||
|
ta.value = text
|
||||||
|
ta.style.position = 'fixed'
|
||||||
|
ta.style.opacity = '0'
|
||||||
|
document.body.appendChild(ta)
|
||||||
|
ta.select()
|
||||||
|
try {
|
||||||
|
document.execCommand('copy')
|
||||||
|
interactionApi.trackMarketingEffect({ action: 'copy', content_preview: text.slice(0, 100) }).catch(() => {})
|
||||||
|
loadStats()
|
||||||
|
uni.showToast({ title: '已复制', icon: 'success' })
|
||||||
|
} catch {
|
||||||
|
uni.showToast({ title: '复制失败,请手动选择复制', icon: 'none' })
|
||||||
|
}
|
||||||
|
document.body.removeChild(ta)
|
||||||
}
|
}
|
||||||
|
|
||||||
const exportCsv = () => {
|
const exportCsv = () => {
|
||||||
@@ -340,6 +374,10 @@ const sendToWhatsapp = (text) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const runCompetitorAnalysis = async () => {
|
const runCompetitorAnalysis = async () => {
|
||||||
|
if (!formData.value.product_name) {
|
||||||
|
uni.showToast({ title: '请先输入产品名称', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
uni.showLoading({ title: '分析中...' })
|
uni.showLoading({ title: '分析中...' })
|
||||||
const res = await marketingApi.competitorAnalysis(
|
const res = await marketingApi.competitorAnalysis(
|
||||||
@@ -349,7 +387,24 @@ const runCompetitorAnalysis = async () => {
|
|||||||
formData.value.target
|
formData.value.target
|
||||||
)
|
)
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
competitorResult.value = typeof res.analysis === 'string' ? res.analysis : JSON.stringify(res.analysis, null, 2)
|
const analysis = res.analysis
|
||||||
|
if (!analysis || Object.keys(analysis).length === 0) {
|
||||||
|
competitorResult.value = '暂无分析结果,请确认产品信息后重试'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const lines = []
|
||||||
|
if (analysis.price_range) lines.push(`💰 价格范围:${analysis.price_range}`)
|
||||||
|
if (analysis.market_trends) lines.push(`📈 市场趋势:${analysis.market_trends}`)
|
||||||
|
if (analysis.key_selling_points && analysis.key_selling_points.length) {
|
||||||
|
lines.push(`\n🏆 核心卖点:\n ${analysis.key_selling_points.map(p => `• ${p}`).join('\n ')}`)
|
||||||
|
}
|
||||||
|
if (analysis.common_keywords && analysis.common_keywords.length) {
|
||||||
|
lines.push(`\n🔑 常见关键词:\n ${analysis.common_keywords.map(k => `• ${k}`).join('\n ')}`)
|
||||||
|
}
|
||||||
|
if (analysis.suggestions && analysis.suggestions.length) {
|
||||||
|
lines.push(`\n💡 建议:\n ${analysis.suggestions.map(s => `• ${s}`).join('\n ')}`)
|
||||||
|
}
|
||||||
|
competitorResult.value = lines.length ? lines.join('\n') : '暂无分析结果'
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
uni.showToast({ title: err.message || '分析失败', icon: 'none' })
|
uni.showToast({ title: err.message || '分析失败', icon: 'none' })
|
||||||
|
|||||||
Reference in New Issue
Block a user