feat 搭建架构骨架

This commit is contained in:
Ironsp
2026-09-24 09:09:47 +08:00
parent dd9f6b77a4
commit 7a2efa3fea
180 changed files with 18967 additions and 9 deletions

View File

@@ -0,0 +1,84 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { BusinessType, PageResult } from '@/types/api'
import type { Case, CaseQuery } from '@/types/business/cases'
import { getCaseCounts, getCaseDetail, getCaseList, toggleFavorite as toggleFavoriteApi } from '@/api/cases'
function createDefaultQuery(): CaseQuery {
return {
category: 'all',
industries: [],
scales: [],
dateRange: null,
keyword: '',
page: 1,
pageSize: 8,
}
}
/** 案例库数据(列表 / 筛选 / 分页 / 收藏) */
export const useCasesStore = defineStore('cases', () => {
const query = ref<CaseQuery>(createDefaultQuery())
const list = ref<Case[]>([])
const total = ref(0)
const counts = ref<Partial<Record<BusinessType, number>>>({})
const currentCase = ref<Case | null>(null)
async function fetchCases(): Promise<void> {
const result: PageResult<Case> = await getCaseList(query.value)
list.value = result.list
total.value = result.total
}
async function fetchCounts(): Promise<void> {
counts.value = await getCaseCounts()
}
function setQuery(next: CaseQuery): void {
query.value = next
}
/** 确定筛选(重置页码后取数) */
async function search(): Promise<void> {
query.value = { ...query.value, page: 1 }
await fetchCases()
}
/** 重置全部条件 */
async function resetQuery(): Promise<void> {
query.value = createDefaultQuery()
await fetchCases()
}
async function changePage(page: number): Promise<void> {
query.value = { ...query.value, page }
await fetchCases()
}
async function fetchCaseDetail(id: string): Promise<void> {
currentCase.value = await getCaseDetail(id)
}
async function toggleFavorite(id: string): Promise<void> {
const updated = await toggleFavoriteApi(id)
const index = list.value.findIndex((item) => item.id === id)
if (index >= 0) list.value[index] = updated
if (currentCase.value?.id === id) currentCase.value = updated
}
return {
query,
list,
total,
counts,
currentCase,
fetchCases,
fetchCounts,
setQuery,
search,
resetQuery,
changePage,
fetchCaseDetail,
toggleFavorite,
}
})

View File

@@ -0,0 +1,79 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { PageResult } from '@/types/api'
import type { Contract, ContractQuery } from '@/types/business/contract'
import {
getContractDetail,
getContractList,
getSignQr,
refreshSignQr,
} from '@/api/contract'
function createDefaultQuery(): ContractQuery {
return {
keyword: '',
page: 1,
pageSize: 8,
}
}
/** 合同中心数据(列表 / 筛选 / 分页 / 详情 / 签署二维码) */
export const useContractStore = defineStore('contract', () => {
const query = ref<ContractQuery>(createDefaultQuery())
const list = ref<Contract[]>([])
const total = ref(0)
const current = ref<Contract | null>(null)
const signExpires = ref(272)
async function fetchList(): Promise<void> {
const result: PageResult<Contract> = await getContractList(query.value)
list.value = result.list
total.value = result.total
}
async function fetchDetail(id: string): Promise<void> {
current.value = await getContractDetail(id)
}
function setQuery(next: ContractQuery): void {
query.value = next
}
/** 确定筛选(重置页码后取数) */
async function search(): Promise<void> {
query.value = { ...query.value, page: 1 }
await fetchList()
}
/** 重置全部条件 */
async function resetQuery(): Promise<void> {
query.value = createDefaultQuery()
await fetchList()
}
async function changePage(page: number): Promise<void> {
query.value = { ...query.value, page }
await fetchList()
}
/** 获取/刷新签署二维码(refresh=true 时走刷新接口) */
async function fetchSignQr(id: string, refresh = false): Promise<void> {
const result = refresh ? await refreshSignQr(id) : await getSignQr(id)
signExpires.value = result.expiresInSeconds
}
return {
query,
list,
total,
current,
signExpires,
fetchList,
fetchDetail,
setQuery,
search,
resetQuery,
changePage,
fetchSignQr,
}
})

View File

@@ -0,0 +1,126 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import type { Demand } from '@/types/business/demand'
import type { FileInfo } from '@/types/api'
import { formatDate } from '@/utils/format'
import { getDraftDemand, saveDraft as saveDraftApi, submitDemand } from '@/api/demand'
function createDefaultForm(): Demand {
return {
businessType: 'internet',
projectName: '',
attachments: [],
coreDescription: '',
expectedDuration: '',
budgetRange: '',
userName: '王永志',
userPhone: '1866****9618',
remark: '',
status: 'draft',
}
}
/** 需求提报数据(M3:表单 / 步骤 / 提交与草稿) */
export const useDemandStore = defineStore('demand', () => {
const form = ref<Demand>(createDefaultForm())
const step = ref(1)
const loading = ref(false)
function setStep(next: number): void {
step.value = next
}
function patchForm(partial: Partial<Demand>): void {
form.value = { ...form.value, ...partial }
}
/** File → FileInfo(mock:url 为空,uploadTime 取当前时间) */
function addAttachment(file: File): void {
const info: FileInfo = {
name: file.name,
size: file.size,
uploadTime: formatDate(new Date()),
url: '',
}
form.value.attachments = [...form.value.attachments, info]
}
function removeAttachment(target: FileInfo): void {
form.value.attachments = form.value.attachments.filter(
(item) => !(item.name === target.name && item.uploadTime === target.uploadTime),
)
}
/** 提交需求:必填校验通过后调 api,成功返回 true(页面可据此跳转) */
async function submit(): Promise<boolean> {
if (!form.value.businessType) {
ElMessage.warning('请选择业务类型')
return false
}
if (!form.value.projectName.trim()) {
ElMessage.warning('请填写项目名称')
return false
}
if (!form.value.coreDescription.trim()) {
ElMessage.warning('请填写核心需求描述')
return false
}
loading.value = true
try {
await submitDemand(form.value)
form.value.status = 'submitted'
ElMessage.success('提交后我们的专业顾问将在一个工作日内与您联系')
return true
}
catch (error) {
ElMessage.error(error instanceof Error ? error.message : '提交失败,请稍后重试')
return false
}
finally {
loading.value = false
}
}
/** 保存草稿到 sessionStorage */
async function saveDraft(): Promise<void> {
loading.value = true
try {
await saveDraftApi(form.value)
form.value.status = 'draft'
ElMessage.success('草稿已保存')
}
catch (error) {
ElMessage.error(error instanceof Error ? error.message : '草稿保存失败')
}
finally {
loading.value = false
}
}
/** 恢复本地草稿(无草稿则保持默认表单) */
async function loadDraft(): Promise<void> {
const draft = await getDraftDemand()
if (draft) form.value = { ...createDefaultForm(), ...draft }
}
/** 重置表单与步骤 */
function reset(): void {
form.value = createDefaultForm()
step.value = 1
}
return {
form,
step,
loading,
setStep,
patchForm,
addAttachment,
removeAttachment,
submit,
saveDraft,
loadDraft,
reset,
}
})

View File

@@ -0,0 +1,207 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { PageResult } from '@/types/api'
import type {
Bill,
BillQuery,
FinanceStats,
Invoice,
InvoiceQuery,
InvoiceTitle,
InvoiceTitlePayload,
PayMethod,
Payment,
PaymentResult,
} from '@/types/business/finance'
import {
applyInvoice as applyInvoiceApi,
createInvoiceTitle,
createPayment as createPaymentApi,
deleteInvoiceTitle,
exportReport as exportReportApi,
getBillDetail,
getBillList,
getFinanceStats,
getInvoiceList,
getInvoiceTitles,
getPayResult,
setDefaultTitle as setDefaultTitleApi,
switchPaymentMethod as switchPaymentMethodApi,
updateInvoiceTitle,
} from '@/api/finance'
function createDefaultBillQuery(): BillQuery {
return { keyword: '', status: 'all', page: 1, pageSize: 6 }
}
function createDefaultInvoiceQuery(): InvoiceQuery {
return { keyword: '', status: 'all', page: 1, pageSize: 6 }
}
/** 财务中心数据(统计/账单/支付/发票/抬头) */
export const useFinanceStore = defineStore('finance', () => {
// —— 统计
const stats = ref<FinanceStats | null>(null)
// —— 账单列表
const list = ref<Bill[]>([])
const total = ref(0)
const query = ref<BillQuery>(createDefaultBillQuery())
const currentBill = ref<Bill | null>(null)
// —— 支付
const payment = ref<Payment | null>(null)
const result = ref<PaymentResult | null>(null)
// —— 发票
const invoices = ref<Invoice[]>([])
const invoiceTotal = ref(0)
const invoiceQuery = ref<InvoiceQuery>(createDefaultInvoiceQuery())
// —— 发票抬头
const titles = ref<InvoiceTitle[]>([])
// —— 统计
async function fetchStats(period?: string): Promise<void> {
stats.value = await getFinanceStats(period)
}
// —— 账单
async function fetchList(): Promise<void> {
const pageData: PageResult<Bill> = await getBillList(query.value)
list.value = pageData.list
total.value = pageData.total
}
function setQuery(next: BillQuery): void {
query.value = next
}
/** 确定筛选(重置页码后取数) */
async function search(): Promise<void> {
query.value = { ...query.value, page: 1 }
await fetchList()
}
/** 重置全部条件 */
async function resetQuery(): Promise<void> {
query.value = createDefaultBillQuery()
await fetchList()
}
async function changePage(page: number): Promise<void> {
query.value = { ...query.value, page }
await fetchList()
}
async function fetchDetail(id: string): Promise<void> {
currentBill.value = await getBillDetail(id)
}
// —— 支付
async function createPayment(billId: string, method: PayMethod): Promise<void> {
payment.value = await createPaymentApi(billId, method)
}
/** 切换支付方式(刷新二维码) */
async function switchPaymentMethod(billId: string, method: PayMethod): Promise<void> {
payment.value = await switchPaymentMethodApi(billId, method)
}
async function fetchResult(billId: string): Promise<void> {
result.value = await getPayResult(billId)
}
// —— 发票
async function fetchInvoices(): Promise<void> {
const pageData: PageResult<Invoice> = await getInvoiceList(invoiceQuery.value)
invoices.value = pageData.list
invoiceTotal.value = pageData.total
}
function setInvoiceQuery(next: InvoiceQuery): void {
invoiceQuery.value = next
}
/** 发票确定筛选(重置页码后取数) */
async function searchInvoices(): Promise<void> {
invoiceQuery.value = { ...invoiceQuery.value, page: 1 }
await fetchInvoices()
}
async function changeInvoicePage(page: number): Promise<void> {
invoiceQuery.value = { ...invoiceQuery.value, page }
await fetchInvoices()
}
/** 申请开票(状态 → 开票中) */
async function applyInvoice(id: string): Promise<void> {
const updated = await applyInvoiceApi(id)
const index = invoices.value.findIndex((item) => item.id === id)
if (index >= 0) invoices.value[index] = updated
}
// —— 发票抬头
async function fetchTitles(): Promise<void> {
titles.value = await getInvoiceTitles()
}
async function createTitle(payload: InvoiceTitlePayload): Promise<void> {
await createInvoiceTitle(payload)
await fetchTitles()
}
async function updateTitle(id: string, payload: InvoiceTitlePayload): Promise<void> {
await updateInvoiceTitle(id, payload)
await fetchTitles()
}
async function deleteTitle(id: string): Promise<void> {
await deleteInvoiceTitle(id)
await fetchTitles()
}
async function setDefaultTitle(id: string): Promise<void> {
titles.value = await setDefaultTitleApi(id)
}
// —— 报表导出
async function exportReport(): Promise<void> {
await exportReportApi()
}
return {
stats,
list,
total,
query,
currentBill,
payment,
result,
invoices,
invoiceTotal,
invoiceQuery,
titles,
fetchStats,
fetchList,
setQuery,
search,
resetQuery,
changePage,
fetchDetail,
createPayment,
switchPaymentMethod,
fetchResult,
fetchInvoices,
setInvoiceQuery,
searchInvoices,
changeInvoicePage,
applyInvoice,
fetchTitles,
createTitle,
updateTitle,
deleteTitle,
setDefaultTitle,
export: exportReport,
}
})

View File

@@ -0,0 +1,46 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { BusinessType } from '@/types/api'
import type { Case } from '@/types/business/cases'
import type { BusinessLine, HomeStats } from '@/types/business/home'
import { getBusinessDetail, getBusinessLines, getFeaturedCases, getHomeStats } from '@/api/home'
/** 首页数据(数据层:取数 + 状态,页面只调本 store) */
export const useHomeStore = defineStore('home', () => {
const stats = ref<HomeStats>({ pending: 0, inProgress: 0, total: 0 })
const businessLines = ref<BusinessLine[]>([])
const featuredCases = ref<Case[]>([])
const currentBusiness = ref<BusinessLine | null>(null)
/** 首页初始化:看板 + 业务线 + 案例精选 */
async function fetchHomeData(): Promise<void> {
const [homeStats, lines, cases] = await Promise.all([
getHomeStats(),
getBusinessLines(),
getFeaturedCases(),
])
stats.value = homeStats
businessLines.value = lines
featuredCases.value = cases
}
/** 按业务类型切换案例精选 */
async function fetchFeaturedCases(category: BusinessType | 'all' = 'all'): Promise<void> {
featuredCases.value = await getFeaturedCases(category)
}
/** 业务详情 */
async function fetchBusinessDetail(type: BusinessType): Promise<void> {
currentBusiness.value = await getBusinessDetail(type)
}
return {
stats,
businessLines,
featuredCases,
currentBusiness,
fetchHomeData,
fetchFeaturedCases,
fetchBusinessDetail,
}
})

View File

@@ -0,0 +1,112 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import type { PageResult } from '@/types/api'
import type { Message, MessageQuery } from '@/types/business/message'
import {
getMessageDetail,
getMessageList,
getTabCounts,
getUnreadCount,
markAllRead as markAllReadApi,
markRead as markReadApi,
} from '@/api/message'
/** tab 计数结构 */
export interface MessageTabCounts {
all: number
在线客服: number
状态通知: number
系统通知: number
}
function createDefaultQuery(): MessageQuery {
return {
keyword: '',
type: 'all',
page: 1,
pageSize: 10,
}
}
/** 消息中心数据(列表 / 筛选 / 分页 / 已读) */
export const useMessageStore = defineStore('message', () => {
const query = ref<MessageQuery>(createDefaultQuery())
const list = ref<Message[]>([])
const total = ref(0)
const current = ref<Message | null>(null)
const unreadCount = ref(0)
const tabCounts = ref<MessageTabCounts>({ all: 0, 在线客服: 0, 状态通知: 0, 系统通知: 0 })
async function fetchList(): Promise<void> {
const result: PageResult<Message> = await getMessageList(query.value)
list.value = result.list
total.value = result.total
}
async function fetchDetail(id: string): Promise<void> {
const detail = await getMessageDetail(id)
current.value = detail
if (detail && !detail.isRead) {
await markReadApi(id)
detail.isRead = true
if (unreadCount.value > 0) unreadCount.value -= 1
await fetchTabCounts()
}
}
function setQuery(next: MessageQuery): void {
query.value = next
}
/** 确定搜索(重置页码后取数) */
async function search(): Promise<void> {
query.value = { ...query.value, page: 1 }
await fetchList()
}
/** 重置全部条件 */
async function resetQuery(): Promise<void> {
query.value = createDefaultQuery()
await fetchList()
}
async function changePage(page: number): Promise<void> {
query.value = { ...query.value, page }
await fetchList()
}
/** 全部标记已读 */
async function markAllRead(): Promise<void> {
await markAllReadApi()
ElMessage.success('已全部标记为已读')
await Promise.all([fetchList(), fetchUnreadCount(), fetchTabCounts()])
if (current.value) current.value.isRead = true
}
async function fetchUnreadCount(): Promise<void> {
unreadCount.value = await getUnreadCount()
}
async function fetchTabCounts(): Promise<void> {
tabCounts.value = await getTabCounts()
}
return {
query,
list,
total,
current,
unreadCount,
tabCounts,
fetchList,
fetchDetail,
setQuery,
search,
resetQuery,
changePage,
markAllRead,
fetchUnreadCount,
fetchTabCounts,
}
})

View File

@@ -0,0 +1,258 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import type { BusinessType, PageResult } from '@/types/api'
import type { Case } from '@/types/business/cases'
import type {
Address,
ChangePasswordParams,
Feedback,
FavoritesQuery,
HelpFaqGroup,
ListQuery,
Profile,
Qualification,
QualificationInput,
} from '@/types/business/profile'
import {
bindEmail,
bindPhone,
changePassword,
createAddress,
createQualification,
deleteAccount as deleteAccountApi,
deleteAddress,
deleteQualification,
getAddresses,
getCaseCounts,
getFavorites,
getHelpFaqs,
getProfile,
getQualifications,
setDefaultAddress,
submitFeedback as submitFeedbackApi,
unbindEmail,
updateAddress,
updateNickname,
updateQualification,
verifyIdentity,
} from '@/api/profile'
import { storage } from '@/utils/storage'
function createDefaultAddressQuery(): ListQuery {
return { page: 1, pageSize: 6 }
}
function createDefaultQualificationQuery(): ListQuery {
return { page: 1, pageSize: 6 }
}
function createDefaultFavoritesQuery(): FavoritesQuery {
return { page: 1, pageSize: 8, keyword: '', category: 'all' }
}
/** 个人中心数据(资料/安全/地址/收藏/资质/反馈/帮助) */
export const useProfileStore = defineStore('profile', () => {
// ── 账号资料 ──
const profile = ref<Profile | null>(null)
async function fetchProfile(): Promise<void> {
profile.value = await getProfile()
}
async function saveNickname(name: string): Promise<void> {
profile.value = await updateNickname(name)
ElMessage.success('昵称修改成功')
}
// ── 账号安全 ──
async function changePasswordAction(params: ChangePasswordParams): Promise<void> {
await changePassword(params)
profile.value = await getProfile()
ElMessage.success('密码修改成功')
}
async function bindPhoneAction(phone: string, code: string): Promise<void> {
profile.value = await bindPhone(phone, code)
ElMessage.success('手机号绑定成功')
}
async function bindEmailAction(email: string, code: string): Promise<void> {
profile.value = await bindEmail(email, code)
ElMessage.success('邮箱绑定成功')
}
async function unbindEmailAction(): Promise<void> {
profile.value = await unbindEmail()
ElMessage.success('已取消邮箱绑定')
}
async function verifyIdentityAction(phone: string, code: string): Promise<void> {
await verifyIdentity(phone, code)
}
async function deleteAccountAction(): Promise<void> {
await deleteAccountApi()
storage.remove('token')
ElMessage.success('注销成功')
}
// ── 收货地址 ──
const addresses = ref<Address[]>([])
const addressTotal = ref(0)
const addressQuery = ref<ListQuery>(createDefaultAddressQuery())
async function fetchAddresses(): Promise<void> {
const result: PageResult<Address> = await getAddresses(addressQuery.value)
addresses.value = result.list
addressTotal.value = result.total
}
async function createAddressAction(input: Omit<Address, 'id' | 'isDefault'>): Promise<void> {
await createAddress(input)
ElMessage.success('新增地址成功')
addressQuery.value = { ...addressQuery.value, page: 1 }
await fetchAddresses()
}
async function updateAddressAction(id: string, input: Omit<Address, 'id' | 'isDefault'>): Promise<void> {
await updateAddress(id, input)
ElMessage.success('地址保存成功')
await fetchAddresses()
}
async function deleteAddressAction(id: string): Promise<void> {
await deleteAddress(id)
ElMessage.success('地址删除成功')
await fetchAddresses()
}
async function setDefaultAddressAction(id: string): Promise<void> {
await setDefaultAddress(id)
ElMessage.success('已设为默认地址')
await fetchAddresses()
}
async function changeAddressPage(page: number): Promise<void> {
addressQuery.value = { ...addressQuery.value, page }
await fetchAddresses()
}
// ── 我的收藏 ──
const favorites = ref<Case[]>([])
const favoritesTotal = ref(0)
const favoritesCounts = ref<Partial<Record<BusinessType, number>>>({})
const favoritesQuery = ref<FavoritesQuery>(createDefaultFavoritesQuery())
async function fetchFavorites(): Promise<void> {
const result: PageResult<Case> = await getFavorites(favoritesQuery.value)
favorites.value = result.list
favoritesTotal.value = result.total
}
async function fetchFavoritesCounts(): Promise<void> {
favoritesCounts.value = await getCaseCounts()
}
async function searchFavorites(patch: Partial<FavoritesQuery>): Promise<void> {
favoritesQuery.value = { ...favoritesQuery.value, ...patch, page: 1 }
await fetchFavorites()
}
async function resetFavorites(): Promise<void> {
favoritesQuery.value = createDefaultFavoritesQuery()
await fetchFavorites()
}
async function changeFavoritesPage(page: number): Promise<void> {
favoritesQuery.value = { ...favoritesQuery.value, page }
await fetchFavorites()
}
// ── 资质管理 ──
const qualifications = ref<Qualification[]>([])
const qualificationTotal = ref(0)
const qualificationQuery = ref<ListQuery>(createDefaultQualificationQuery())
async function fetchQualifications(): Promise<void> {
const result: PageResult<Qualification> = await getQualifications(qualificationQuery.value)
qualifications.value = result.list
qualificationTotal.value = result.total
}
async function createQualificationAction(input: QualificationInput): Promise<void> {
await createQualification(input)
ElMessage.success('资质提交成功')
qualificationQuery.value = { ...qualificationQuery.value, page: 1 }
await fetchQualifications()
}
async function updateQualificationAction(id: string, input: QualificationInput): Promise<void> {
await updateQualification(id, input)
ElMessage.success('资质保存成功')
await fetchQualifications()
}
async function deleteQualificationAction(id: string): Promise<void> {
await deleteQualification(id)
ElMessage.success('资质删除成功')
await fetchQualifications()
}
async function changeQualificationPage(page: number): Promise<void> {
qualificationQuery.value = { ...qualificationQuery.value, page }
await fetchQualifications()
}
// ── 意见反馈 / 帮助中心 ──
async function submitFeedbackAction(feedback: Feedback): Promise<void> {
await submitFeedbackApi(feedback)
}
const helpFaqs = ref<HelpFaqGroup[]>([])
async function fetchHelpFaqs(): Promise<void> {
helpFaqs.value = await getHelpFaqs()
}
return {
profile,
fetchProfile,
saveNickname,
changePassword: changePasswordAction,
bindPhone: bindPhoneAction,
bindEmail: bindEmailAction,
unbindEmail: unbindEmailAction,
verifyIdentity: verifyIdentityAction,
deleteAccount: deleteAccountAction,
addresses,
addressTotal,
addressQuery,
fetchAddresses,
createAddress: createAddressAction,
updateAddress: updateAddressAction,
deleteAddress: deleteAddressAction,
setDefaultAddress: setDefaultAddressAction,
changeAddressPage,
favorites,
favoritesTotal,
favoritesCounts,
favoritesQuery,
fetchFavorites,
fetchFavoritesCounts,
searchFavorites,
resetFavorites,
changeFavoritesPage,
qualifications,
qualificationTotal,
qualificationQuery,
fetchQualifications,
createQualification: createQualificationAction,
updateQualification: updateQualificationAction,
deleteQualification: deleteQualificationAction,
changeQualificationPage,
submitFeedback: submitFeedbackAction,
helpFaqs,
fetchHelpFaqs,
}
})

View File

@@ -0,0 +1,106 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import type { PageResult } from '@/types/api'
import type { AfterSalesPayload, Project, ProjectQuery } from '@/types/business/project'
import {
cancelAfterSales as cancelAfterSalesApi,
deleteProjectFile,
getProjectDetail,
getProjectList,
submitAfterSales as submitAfterSalesApi,
uploadProjectFile,
} from '@/api/project'
function createDefaultQuery(): ProjectQuery {
return {
keyword: '',
businessType: 'all',
status: 'all',
timeShortcut: 'all',
page: 1,
pageSize: 8,
}
}
/** 项目中心数据(列表 / 筛选 / 分页 / 详情 / 文件 / 售后) */
export const useProjectStore = defineStore('project', () => {
const query = ref<ProjectQuery>(createDefaultQuery())
const list = ref<Project[]>([])
const total = ref(0)
const current = ref<Project | null>(null)
async function fetchList(): Promise<void> {
const result: PageResult<Project> = await getProjectList(query.value)
list.value = result.list
total.value = result.total
}
async function fetchDetail(id: string): Promise<void> {
current.value = await getProjectDetail(id)
}
function setQuery(next: ProjectQuery): void {
query.value = next
}
/** 确定筛选(重置页码后取数) */
async function search(): Promise<void> {
query.value = { ...query.value, page: 1 }
await fetchList()
}
/** 重置全部条件 */
async function resetQuery(): Promise<void> {
query.value = createDefaultQuery()
await fetchList()
}
async function changePage(page: number): Promise<void> {
query.value = { ...query.value, page }
await fetchList()
}
/** 上传项目文件(追加到当前详情) */
async function uploadFile(file: File): Promise<void> {
const info = await uploadProjectFile(file)
current.value?.files.push(info)
}
/** 删除当前详情中的项目文件 */
async function deleteFile(index: number): Promise<void> {
await deleteProjectFile(index)
current.value?.files.splice(index, 1)
}
/** 提交售后工单 */
async function submitAfterSales(payload: AfterSalesPayload): Promise<void> {
if (!current.value) return
current.value.afterSales = await submitAfterSalesApi(current.value.id, payload)
ElMessage.success('已提交售后工单')
}
/** 取消售后工单 */
async function cancelAfterSales(): Promise<void> {
if (!current.value) return
current.value.afterSales = await cancelAfterSalesApi(current.value.id)
ElMessage.success('已取消售后工单')
}
return {
query,
list,
total,
current,
fetchList,
fetchDetail,
setQuery,
search,
resetQuery,
changePage,
uploadFile,
deleteFile,
submitAfterSales,
cancelAfterSales,
}
})

View File

@@ -0,0 +1,60 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { ChatMessage } from '@/types/business/service'
import {
getChatHistory,
mockAutoReply,
sendChatMessage,
uploadChatFile,
} from '@/api/service'
/** 在线客服会话数据(消息列表 / 发送状态) */
export const useServiceStore = defineStore('service', () => {
const messages = ref<ChatMessage[]>([])
const sending = ref(false)
async function fetchHistory(): Promise<void> {
messages.value = await getChatHistory()
}
/** 发送文本:先推入用户消息,600ms 后推入客服自动回复 */
async function send(content: string): Promise<void> {
const trimmed = content.trim()
if (!trimmed || sending.value) return
sending.value = true
try {
const userMsg = await sendChatMessage(trimmed)
messages.value.push(userMsg)
window.setTimeout(() => {
void mockAutoReply().then((reply) => {
messages.value.push(reply)
sending.value = false
})
}, 600)
}
catch {
sending.value = false
}
}
/** 发送文件卡片消息 */
async function sendFile(file: File): Promise<void> {
if (sending.value) return
sending.value = true
try {
const fileMsg = await uploadChatFile(file)
messages.value.push(fileMsg)
}
finally {
sending.value = false
}
}
return {
messages,
sending,
fetchHistory,
send,
sendFile,
}
})

View File

@@ -0,0 +1,24 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
/** 站点公共数据(站点配置、全局 loading),非页面级 */
export const useSiteStore = defineStore('site', () => {
const globalLoading = ref(false)
// TODO(api): 站点配置接入后由 fetchSiteConfig 覆盖默认值
const siteInfo = ref({
appName: '虫子智造',
hotline: '400-XXX-XXXX',
businessEmail: 'business@chongzizhizao.com',
supportEmail: 'support@chongzizhizao.com',
workTime: '周一-周五 9:00-18:00',
icp: 'ICP 备 XXXXXXXX 号',
police: '公网安备 XXXXXXXXXXXX 号',
})
function setGlobalLoading(value: boolean): void {
globalLoading.value = value
}
return { globalLoading, siteInfo, setGlobalLoading }
})

137
src/stores/modules/user.ts Normal file
View File

@@ -0,0 +1,137 @@
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { ElMessage } from 'element-plus'
import type {
LoginByAccountParams,
LoginBySmsParams,
LoginResult,
RegisterParams,
ResetPasswordParams,
User,
} from '@/types/business/user'
import {
loginByAccount as loginByAccountApi,
loginBySms as loginBySmsApi,
register as registerApi,
resetPassword as resetPasswordApi,
sendSmsCode as sendSmsCodeApi,
} from '@/api/auth'
import { storage } from '@/utils/storage'
/** 用户登录态(M4:token 持久化 key 'token',路由守卫读取同一 key) */
export const useUserStore = defineStore('user', () => {
const token = ref<string | null>(storage.get<string>('token'))
const user = ref<User | null>(null)
const loading = ref(false)
const isLoggedIn = computed(() => Boolean(token.value))
/** 发送短信验证码 */
async function sendSmsCode(phone: string): Promise<void> {
try {
await sendSmsCodeApi(phone)
ElMessage.success('验证码已发送')
}
catch (error) {
ElMessage.error(error instanceof Error ? error.message : '验证码发送失败')
throw error
}
}
/** 登录成功统一落库(token 持久化) */
function applyLogin(result: LoginResult): void {
token.value = result.token
user.value = result.user
storage.set('token', result.token)
}
/** 账号密码登录 */
async function loginByAccount(params: LoginByAccountParams): Promise<boolean> {
loading.value = true
try {
const result = await loginByAccountApi(params)
applyLogin(result)
ElMessage.success('登录成功')
return true
}
catch (error) {
ElMessage.error(error instanceof Error ? error.message : '登录失败')
return false
}
finally {
loading.value = false
}
}
/** 手机验证码登录 */
async function loginBySms(params: LoginBySmsParams): Promise<boolean> {
loading.value = true
try {
const result = await loginBySmsApi(params)
applyLogin(result)
ElMessage.success('登录成功')
return true
}
catch (error) {
ElMessage.error(error instanceof Error ? error.message : '登录失败')
return false
}
finally {
loading.value = false
}
}
/** 注册 */
async function register(params: RegisterParams): Promise<boolean> {
loading.value = true
try {
await registerApi(params)
ElMessage.success('注册成功,请登录')
return true
}
catch (error) {
ElMessage.error(error instanceof Error ? error.message : '注册失败')
return false
}
finally {
loading.value = false
}
}
/** 重置密码 */
async function resetPassword(params: ResetPasswordParams): Promise<boolean> {
loading.value = true
try {
await resetPasswordApi(params)
ElMessage.success('修改成功,请登录')
return true
}
catch (error) {
ElMessage.error(error instanceof Error ? error.message : '修改失败')
return false
}
finally {
loading.value = false
}
}
/** 退出登录(清除持久化 token) */
function logout(): void {
token.value = null
user.value = null
storage.remove('token')
}
return {
token,
user,
loading,
isLoggedIn,
sendSmsCode,
loginByAccount,
loginBySms,
register,
resetPassword,
logout,
}
})