Spinner Model Controls(二)

JSpinner 模型详解
本文介绍了JSpinner组件中的SpinnerModel接口及其三种实现:SpinnerDateModel、SpinnerListModel和SpinnerNumberModel。探讨了如何通过这些模型定制日期、列表项和数值的选择,并提供了自定义模型的示例。

14.2 SpinnerModel接口

到目前为止,我们已经了解了如何与主JSpinner类交互。SpinnerModel接口是组件的数据模型。SpinnerModel的定义如下:

public interface SpinnerModel {
  // Properties
  public Object getValue();
  public void setValue(Object);
  public Object getNextValue();
  public Object getPreviousValue();
  // Listeners
  public void addChangeListener(ChangeListener);
  public void removeChangeListener(ChangeListener);
}

SpinnerMOdel的六个方法直接映射到JSpinner的相应方法。JSpinner方法只是将这些方法调用转向模型的方法,尽管在监听器方法的情况下,事件源是我们关联监听器的地方。

14.3 AbstractSpinnerModel类

SpinnerModel接口的基本实现是AbstractSpinnerModel类。他提供了监听器列表的管理与通知。子类必须实现其他的四个与值相关的接口方法。SpinnerModel接口的实现有:SpinnerDateModel,SpinnerListModel与SpinnerNumberModel。

14.3.1 SpinnerDateModel类

正如其名字所暗示的,SpinnerModel提供了日期的选择。这个类有两个构造函数:一个在默认情况下选择所有的日期,而另一个允许我们限制范围。


  
public SpinnerDateModel()
SpinnerModel model = new SpinnerDateModel();
JSpinner spinner = new JSpinner(model);
public SpinnerDateModel(Date value, Comparable start, Comparable end,
  int calendarField)
Calendar cal = Calendar.getInstance();
Date now = cal.getTime();
cal.add(Calendar.YEAR, -50);
Date startDate = cal.getTime();
cal.add(Calendar.YEAR, 100);
Date endDate = cal.getTime();
SpinnerModel model =
  new SpinnerDateModel(now, startDate, endDate, Calendar.YEAR);
JSpinner spinner = new JSpinner(model);

如果我们没有指定任何参数,则没有起始点与结束点。这里所显示的示例使用参数来提供100年的范围。最后一个域应是Calendar类的一个常量:

•Calendar.AM_PM

•Calendar.DAY_OF_MONTH

•Calendar.DAY_OF_WEEK

•Calendar.DAY_OF_WEEK_IN_MONTH

•Calendar.DAY_OF_YEAR

•Calendar.ERA

•Calendar.HOUR

•Calendar.HOUR_OF_DAY

•Calendar.MILLISECOND

•Calendar.MINUTE

•Calendar.MONTH

•Calendar.SECOND

•Calendar.WEEK_OF_MONTH

•Calendar.WEEK_OF_YEAR

• Calendar.YEAR

注意,SpinnerModel不包含任何与时区相关的Calendar常量。我们不可以通过SpinnerDateModel在JSpinner内进行滚动。

表14-3列出了SpinnerModel接口的三个属性以四个SpinnerDateModel的特定属性。


通常情况下,我们将会使用的唯一新属性是用于获取最终的日期,尽管他所做的是以合适的数据类型包装getValue()的方法的结果。如果我们为构造函数提供了一个日期范围,在当前值为边界条件时,前一个或是后一个值将为null。

14.3.2 SpinnerListModel类

SpinnerListModel提供了由条目列表中,或者是至少是他们的字符串表示中进行选择。这个类有三个构造函数:


  
public SpinnerListModel()
SpinnerModel model = new SpinnerListModel();
JSpinner spinner = new JSpinner(model);
public SpinnerListModel(List<?> values)
List<String> list = args;
SpinnerModel model = new SpinnerListModel(list);
JSpinner spinner = new JSpinner(model);
public SpinnerListModel(Object[] values)
SpinnerModel model = new SpinnerListModel(args);
JSpinner spinner = new JSpinner(model);

当没有提供参数时,模型只包含一个元素:字符串empty。List版本具有一个到列表的引用。他并没有拷贝这个列表。如果我们修改这个列表,我们就修改了模型中的元素。数组版本创建了一个不可以添加的私有的List实例的内联类。对于List与数组版本,初始时选中的是第一个元素。如果其中一个为空,则会抛出IllegalArgumentException。

如表14-4所示,在接口之外所添加的唯一属性就是获取或是设置列表。


14.3.3 SpinnerNumberModel类

SpinnerNumberModel提供了由一个无限制或是有限制的值范围内进行数字选择。所选择的数字可以是Number的任意子类,包括Integer与Double。这个类具有四个构造函数,而前三个都是最后一个的简化版。


  
public SpinnerNumberModel()
SpinnerModel model = new SpinnerNumberModel();
JSpinner spinner = new JSpinner(model);
public SpinnerNumberModel(double value, double minimum, double maximum,
  double stepSize)
SpinnerModel model = new SpinnerNumberModel(50, 0, 100, .25);
JSpinner spinner = new JSpinner(model);
public SpinnerNumberModel(int value, int minimum, int maximum, int stepSize)
SpinnerModel model = new SpinnerNumberModel(50, 0, 100, 1);
JSpinner spinner = new JSpinner(model);
public SpinnerNumberModel(Number value, Comparable minimum, Comparable maximum,
  Number stepSize)
Number value = new Integer(50);
Number min = new Integer(0);
Number max = new Integer(100);
Number step = new Integer(1);
SpinnerModel model = new SpinnerNumberModel(value, min, max, step);
JSpinner spinner = new JSpinner(model);

如果最小值或是最大值为null,则这个范围就是无限制的。对于无参数的版本,初始值为0而步进值为1。步进尺寸是字面值,所以如果我们将这个步进值设置为.333,则并不完美。

表14-5显示了SpinnerNumberModel的属性。所添加的属性与构造函数所提供的相同。


14.3.4 自定义模型

通常情况下,JSpinner的可用模型就足够了,所以我们并不需要派生。然而,所提供的模型并不能总是满足我们的需求。例如,我们也许希望使用一个包装了SpinnerListModel的自定义模型,而不希望停在第一个或是最后一个元素上,他向另一个方向环绕。列表14-2显示了一个这样的实现。


  
package swingstudy.ch13;
 
import java.util.List;
 
import javax.swing.SpinnerListModel;
 
public class RolloverSpinnerListModel extends SpinnerListModel {
 
	public RolloverSpinnerListModel(List<?> values) {
		super(values);
	}
 
	public RolloverSpinnerListModel(Object[] values) {
		super(values);
	}
 
	public Object getNextValue() {
		Object returnValue = super.getNextValue();
		if(returnValue == null) {
			returnValue = getList().get(0);
		}
		return returnValue;
	}
 
	public Object getPreviousValue() {
		Object returnValue = super.getPreviousValue();
		if(returnValue == null) {
			List list = getList();
			returnValue = list.get(list.size()-1);
		}
		return returnValue;
	}
}


以下mrprncrud.vue的style比照mrmncrud.vue,其餘功能不變: mrprncrud.vue: <div class="prncrud-container"> <div class="prncrud-controls"> <input v-model="searchQuery" placeholder="搜索處方名稱..." class="search-input" @input="filterPRNTags" /> </div> <div class="form-section"> <form @submit.prevent="createPRNTag" class="create-form"> <input v-model="newPRNTag.prnname" placeholder="輸入新處方名稱" required class="form-input" :disabled="isLoading" /> <button type="submit" class="form-btn create-btn" :disabled="isLoading">創建</button> </form> </div> <div class="prncrud-content"> <div v-if="isLoading" class="loading">加載中...</div> <div v-else> <div class="prncrud-list" v-if="filteredTags.length > 0"> <div v-for="tag in filteredTags" :key="tag.id" class="prncrud-item"> <div class="tag-info"> <span class="tag-id">ID: {{ tag.id }}</span> <span class="tag-name">{{ tag.prnname }}</span> </div> <div class="tag-actions"> <button @click="editPRNTag(tag)" class="action-btn edit-btn">編輯</button> <button @click="deletePRNTag(tag.id)" class="action-btn delete-btn">刪除</button> </div> </div> </div> <div v-else class="no-tags">沒有找到匹配的處方標籤</div> </div> </div> <div v-if="editingTag" class="edit-modal"> <div class="modal-content"> <h3>編輯處方名稱</h3> <form @submit.prevent="updatePRNTag"> <input v-model="editingTag.prnname" required class="form-input" :disabled="isLoading" /> <div class="modal-actions"> <button type="submit" class="form-btn save-btn">保存</button> <button type="button" @click="cancelEdit" class="form-btn cancel-btn">取消</button> </div> </form> </div> </div> </div> </template> <script> import { secureFetch } from '../utils/request'; export default { name: 'mrprncrud', emits: ['tag-updated'], data() { return { prnTags: [], filteredTags: [], searchQuery: '', newPRNTag: { prnname: '' }, editingTag: null, isLoading: false }; }, methods: { async fetchPRNTags() { this.isLoading = true; try { const response = await fetch('PRNTag/?format=json'); if (!response.ok) throw new Error('獲取數據失敗'); this.prnTags = await response.json(); this.filterPRNTags(); } catch (error) { console.error('獲取處方標籤失敗:', error); alert('加載數據失敗,請重試'); } finally { this.isLoading = false; } }, filterPRNTags() { const query = this.searchQuery.trim().toLowerCase(); this.filteredTags = query ? this.prnTags.filter(tag => tag.prnname.toLowerCase().includes(query) || tag.id.toString().includes(query) ) : [...this.prnTags]; }, async createPRNTag() { if (!this.newPRNTag.prnname.trim()) { alert('處方名稱不能為空'); return; } this.isLoading = true; try { const response = await secureFetch('PRNTag/?format=json', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(this.newPRNTag) }); if (!response.ok) throw new Error('創建失敗'); await this.fetchPRNTags(); this.newPRNTag.prnname = ''; this.$emit('tag-updated'); } catch (error) { console.error('創建處方標籤失敗:', error); alert(error.message); } finally { this.isLoading = false; } }, editPRNTag(tag) { this.editingTag = { ...tag }; }, async updatePRNTag() { if (!this.editingTag.prnname.trim()) { alert('處方名稱不能為空'); return; } this.isLoading = true; try { const response = await secureFetch(`PRNTag/${this.editingTag.id}/?format=json`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(this.editingTag) }); if (!response.ok) throw new Error('更新失敗'); await this.fetchPRNTags(); this.editingTag = null; this.$emit('tag-updated'); } catch (error) { console.error('更新處方標籤失敗:', error); alert(error.message); } finally { this.isLoading = false; } }, async deletePRNTag(id) { if (!confirm('確定要刪除這個處方標籤嗎?')) return; this.isLoading = true; try { const response = await secureFetch(`PRNTag/${id}/?format=json`, { method: 'DELETE' }); if (!response.ok) throw new Error('刪除失敗'); await this.fetchPRNTags(); this.$emit('tag-updated'); } catch (error) { console.error('刪除處方標籤失敗:', error); alert(error.message); } finally { this.isLoading = false; } }, cancelEdit() { this.editingTag = null; } }, mounted() { this.fetchPRNTags(); } }; </script> <style scoped> .prncrud-container { height: 100%; display: flex; flex-direction: column; } .prncrud-controls { padding: 10px; background: #f5f5f5; border-bottom: 1px solid #ddd; } .search-input { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; } .prncrud-content { flex: 1; overflow-y: auto; padding: 10px; } .prncrud-item { display: flex; justify-content: space-between; align-items: center; padding: 10px; background: white; border: 1px solid #eee; border-radius: 4px; margin-bottom: 5px; } .edit-btn { background-color: #2196F3; color: white; } .delete-btn { background-color: #f44336; color: white; } </style> mrmncrud.vue: <div class="mncrud-container"> <div class="mncrud-controls"> <input v-model="searchQuery" placeholder="搜索本草名稱..." class="search-input" @input="filterMNTags" /> </div> <!-- 创建表单 --> <div class="form-section"> <form @submit.prevent="createMNTag" class="create-form"> <input v-model="newMNTag.mnname" placeholder="輸入新本草名稱" required class="form-input" :disabled="isLoading" /> <button type="submit" class="form-btn create-btn" :disabled="isLoading">創建</button> </form> </div> <div class="mncrud-content"> <div v-if="isLoading" class="loading"> <div class="loading-spinner"></div> <span>加載中...</span> </div> <div v-else> <!-- 标签列表 --> <div class="mncrud-list" v-if="filteredTags.length > 0"> <div v-for="tag in filteredTags" :key="tag.id" class="mncrud-item"> <div class="tag-info"> <span class="tag-id">ID: {{ tag.id }}</span> <span class="tag-name">{{ tag.mnname }}</span> </div> <div class="tag-actions"> <button @click="editMNTag(tag)" class="action-btn edit-btn">編輯</button> <button @click="deleteMNTag(tag.id)" class="action-btn delete-btn">刪除</button> </div> </div> </div> <div v-else class="no-tags"> 沒有找到匹配的本草標籤 </div> </div> </div> <!-- 编辑模态框 --> <div v-if="editingTag" class="edit-modal"> <div class="modal-content"> <h3>編輯本草名稱</h3> <form @submit.prevent="updateMNTag"> <input v-model="editingTag.mnname" required class="form-input" :disabled="isLoading" /> <div class="modal-actions"> <button type="submit" class="form-btn save-btn">保存</button> <button type="button" @click="cancelEdit" class="form-btn cancel-btn">取消</button> </div> </form> </div> </div> </div> </template> <script> import { secureFetch } from '../utils/request'; export default { name: 'mrmncrud', data() { return { mnTags: [], filteredTags: [], searchQuery: '', newMNTag: { mnname: '' }, editingTag: null, isLoading: false }; }, methods: { async fetchMNTags() { this.isLoading = true; try { const response = await fetch('MNTag/?format=json'); if (!response.ok) throw new Error('获取数据失败'); this.mnTags = await response.json(); this.filterMNTags(); } catch (error) { console.error('获取本草标签失败:', error); alert('加载数据失败,请重试'); } finally { this.isLoading = false; } }, filterMNTags() { const query = this.searchQuery.trim().toLowerCase(); if (!query) { this.filteredTags = [...this.mnTags]; } else { this.filteredTags = this.mnTags.filter(tag => tag.mnname.toLowerCase().includes(query) || tag.id.toString().includes(query) ); } }, async createMNTag() { if (!this.newMNTag.mnname.trim()) { alert('本草名称不能为空'); return; } this.isLoading = true; try { const response = await secureFetch('MNTag/?format=json', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(this.newMNTag), }); if (!response.ok) throw new Error('创建失败'); await this.fetchMNTags(); this.newMNTag.mnname = ''; this.$emit('tag-updated'); } catch (error) { console.error('创建本草标签失败:', error); alert(error.message); } finally { this.isLoading = false; } }, editMNTag(tag) { this.editingTag = { ...tag }; }, async updateMNTag() { if (!this.editingTag.mnname.trim()) { alert('本草名称不能为空'); return; } this.isLoading = true; try { const response = await secureFetch(`MNTag/${this.editingTag.id}/?format=json`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(this.editingTag), }); if (!response.ok) throw new Error('更新失败'); await this.fetchMNTags(); this.editingTag = null; this.$emit('tag-updated'); } catch (error) { console.error('更新本草标签失败:', error); alert(error.message); } finally { this.isLoading = false; } }, cancelEdit() { this.editingTag = null; }, async deleteMNTag(id) { if (!confirm('确定要删除这个本草标签吗?')) return; this.isLoading = true; try { const response = await secureFetch(`MNTag/${id}/?format=json`, { method: 'DELETE' }); if (!response.ok) throw new Error('删除失败'); await this.fetchMNTags(); this.$emit('tag-updated'); } catch (error) { console.error('删除本草标签失败:', error); alert(error.message); } finally { this.isLoading = false; } } }, mounted() { this.fetchMNTags(); } }; </script> <style scoped> .mncrud-container { height: 100%; display: flex; flex-direction: column; } .mncrud-controls { padding: 10px; background: #f5f5f5; border-bottom: 1px solid #ddd; } .search-input { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; } .mncrud-content { flex: 1; overflow-y: auto; padding: 10px; } .form-section { padding: 10px; background: #f5f5f5; border-bottom: 1px solid #ddd; } .create-form { display: flex; gap: 10px; } .form-input { flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px; } .form-btn { padding: 8px 15px; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; } .create-btn { background-color: #4CAF50; color: white; } .mncrud-list { display: flex; flex-direction: column; gap: 10px; } .mncrud-item { display: flex; justify-content: space-between; align-items: center; padding: 10px; background: white; border: 1px solid #eee; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } .tag-info { display: flex; flex-direction: column; } .tag-id { font-size: 0.8rem; color: #666; } .tag-name { font-weight: 500; } .tag-actions { display: flex; gap: 5px; } .action-btn { padding: 5px 10px; border: none; border-radius: 4px; cursor: pointer; font-size: 0.9rem; } .edit-btn { background-color: #2196F3; color: white; } .delete-btn { background-color: #f44336; color: white; } .no-tags { padding: 20px; text-align: center; color: #666; font-style: italic; } .edit-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); display: flex; justify-content: center; align-items: center; z-index: 1000; } .modal-content { background: white; padding: 20px; border-radius: 8px; width: 90%; max-width: 400px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); } .modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 15px; } .save-btn { background-color: #4CAF50; color: white; } .cancel-btn { background-color: #9E9E9E; color: white; } .loading { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20px; } .loading-spinner { border: 4px solid rgba(0, 0, 0, 0.1); border-radius: 50%; border-top: 4px solid #3498db; width: 30px; height: 30px; animation: spin 1s linear infinite; margin-bottom: 10px; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style>
最新发布
10-17
以上mrviewer.vue中的本草關聯功能以前述病態關聯功能為範例進行修改, mrmncrud.vue: <template> <div class="mncrud-container"> <div class="mncrud-controls"> <input v-model="searchQuery" placeholder="搜索本草名稱..." class="search-input" @input="filterMNTags" /> </div> <!-- 创建表单 --> <div class="form-section"> <form @submit.prevent="createMNTag" class="create-form"> <input v-model="newMNTag.mnname" placeholder="輸入新本草名稱" required class="form-input" :disabled="isLoading" /> <button type="submit" class="form-btn create-btn" :disabled="isLoading">創建</button> </form> </div> <div class="mncrud-content"> <div v-if="isLoading" class="loading"> <div class="loading-spinner"></div> <span>加載中...</span> </div> <div v-else> <!-- 标签列表 --> <div class="mncrud-list" v-if="filteredTags.length > 0"> <div v-for="tag in filteredTags" :key="tag.id" class="mncrud-item"> <div class="tag-info"> <span class="tag-id">ID: {{ tag.id }}</span> <span class="tag-name">{{ tag.mnname }}</span> </div> <div class="tag-actions"> <button @click="editMNTag(tag)" class="action-btn edit-btn">編輯</button> <button @click="deleteMNTag(tag.id)" class="action-btn delete-btn">刪除</button> </div> </div> </div> <div v-else class="no-tags"> 沒有找到匹配的本草標籤 </div> </div> </div> <!-- 编辑模态框 --> <div v-if="editingTag" class="edit-modal"> <div class="modal-content"> <h3>編輯本草名稱</h3> <form @submit.prevent="updateMNTag"> <input v-model="editingTag.mnname" required class="form-input" :disabled="isLoading" /> <div class="modal-actions"> <button type="submit" class="form-btn save-btn">保存</button> <button type="button" @click="cancelEdit" class="form-btn cancel-btn">取消</button> </div> </form> </div> </div> </div> </template> <script> export default { name: 'mrmncrud', data() { return { mnTags: [], filteredTags: [], searchQuery: '', newMNTag: { mnname: '' }, editingTag: null, isLoading: false }; }, methods: { async fetchMNTags() { this.isLoading = true; try { const response = await fetch('MNTag/?format=json'); if (!response.ok) throw new Error('获取数据失败'); this.mnTags = await response.json(); this.filterMNTags(); } catch (error) { console.error('获取本草标签失败:', error); alert('加载数据失败,请重试'); } finally { this.isLoading = false; } }, filterMNTags() { const query = this.searchQuery.trim().toLowerCase(); if (!query) { this.filteredTags = [...this.mnTags]; } else { this.filteredTags = this.mnTags.filter(tag => tag.mnname.toLowerCase().includes(query) || tag.id.toString().includes(query) ); } }, async createMNTag() { if (!this.newMNTag.mnname.trim()) { alert('本草名称不能为空'); return; } this.isLoading = true; try { const response = await fetch('MNTag/?format=json', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(this.newMNTag), }); if (!response.ok) throw new Error('创建失败'); await this.fetchMNTags(); this.newMNTag.mnname = ''; this.$emit('tag-updated'); } catch (error) { console.error('创建本草标签失败:', error); alert(error.message); } finally { this.isLoading = false; } }, editMNTag(tag) { this.editingTag = { ...tag }; }, async updateMNTag() { if (!this.editingTag.mnname.trim()) { alert('本草名称不能为空'); return; } this.isLoading = true; try { const response = await fetch(`MNTag/${this.editingTag.id}/?format=json`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(this.editingTag), }); if (!response.ok) throw new Error('更新失败'); await this.fetchMNTags(); this.editingTag = null; this.$emit('tag-updated'); } catch (error) { console.error('更新本草标签失败:', error); alert(error.message); } finally { this.isLoading = false; } }, cancelEdit() { this.editingTag = null; }, async deleteMNTag(id) { if (!confirm('确定要删除这个本草标签吗?')) return; this.isLoading = true; try { const response = await fetch(`MNTag/${id}/?format=json`, { method: 'DELETE' }); if (!response.ok) throw new Error('删除失败'); await this.fetchMNTags(); this.$emit('tag-updated'); } catch (error) { console.error('删除本草标签失败:', error); alert(error.message); } finally { this.isLoading = false; } } }, mounted() { this.fetchMNTags(); } }; </script> <style scoped> .mncrud-container { height: 100%; display: flex; flex-direction: column; } .mncrud-controls { padding: 10px; background: #f5f5f5; border-bottom: 1px solid #ddd; } .search-input { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; } .mncrud-content { flex: 1; overflow-y: auto; padding: 10px; } .form-section { padding: 10px; background: #f5f5f5; border-bottom: 1px solid #ddd; } .create-form { display: flex; gap: 10px; } .form-input { flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px; } .form-btn { padding: 8px 15px; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; } .create-btn { background-color: #4CAF50; color: white; } .mncrud-list { display: flex; flex-direction: column; gap: 10px; } .mncrud-item { display: flex; justify-content: space-between; align-items: center; padding: 10px; background: white; border: 1px solid #eee; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } .tag-info { display: flex; flex-direction: column; } .tag-id { font-size: 0.8rem; color: #666; } .tag-name { font-weight: 500; } .tag-actions { display: flex; gap: 5px; } .action-btn { padding: 5px 10px; border: none; border-radius: 4px; cursor: pointer; font-size: 0.9rem; } .edit-btn { background-color: #2196F3; color: white; } .delete-btn { background-color: #f44336; color: white; } .no-tags { padding: 20px; text-align: center; color: #666; font-style: italic; } .edit-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); display: flex; justify-content: center; align-items: center; z-index: 1000; } .modal-content { background: white; padding: 20px; border-radius: 8px; width: 90%; max-width: 400px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); } .modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 15px; } .save-btn { background-color: #4CAF50; color: white; } .cancel-btn { background-color: #9E9E9E; color: white; } .loading { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20px; } .loading-spinner { border: 4px solid rgba(0, 0, 0, 0.1); border-radius: 50%; border-top: 4px solid #3498db; width: 30px; height: 30px; animation: spin 1s linear infinite; margin-bottom: 10px; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style>
08-04
<!-- * @Description: 海淀智慧物业app- --> <template> <div class="economicdevelopment-page overflow_div page-background scoll_box dashboard-page"> <titlepage v-if="pathStr !== '/'" :title="pathName" :query="query"></titlepage> <!-- <div class="top_box" @click.stop="checkboxGroupShowFn">--> <!-- <div class="right_icon">--> <!-- <van-icon name="arrow-down" v-if="!checkboxGroupShow" />--> <!-- <van-icon name="arrow-up" v-if="checkboxGroupShow" />--> <!-- </div>--> <!-- 下拉多选查询 --> <!-- <p v-for="(item,i) of checkboxGroup" :key="i" @click.stop="deleteCheckbox(item)">--> <!-- <span>{{ item.name }}</span>--> <!-- <van-icon name="close" style="transform: scale(1.2);" />--> <!-- </p>--> <!-- <div v-if="checkboxGroup.length===0" class="placeholder">请选择小区</div>--> <!-- </div>--> <!-- 0805 增加查询项--> <div class="userLoginSearch"> <a-row :gutter="10"> <a-col :span="12"> <a-select show-search v-model="pane.szjz" :filterOption="filterOption" style="width: 100%" @change="handleStreetChange" placeholder="街镇"> <a-select-option :value="item.id" v-for="(item, i) in streetColumns" :key="i" :lable="item.departName"> {{ item.departName }} </a-select-option> </a-select> </a-col> <a-col :span="12"> <a-select show-search v-model="pane.sqmc" :filterOption="filterOption" style="width: 100%" @change="handleSQChange" placeholder="社区"> <a-select-option :value="item.id" v-for="(item, i) in townColumns" :key="i" :lable="item.departName"> {{ item.departName }} </a-select-option> </a-select> </a-col> </a-row> <a-row :gutter="10" style="margin-top: 10px;"> <a-col :span="12"> <a-input v-model="pane.xmmc" placeholder="项目名称"> <a-icon slot="suffix" type="search" style="color:#3b77b3;" /> </a-input> </a-col> <a-col :span="12" style="margin-top: 2px;"> <div class="checkboxContainer"> <span>专业化物业</span> <a-checkbox v-model="sfwzyhwyValue" @change="changeZyhwy" /> </div> </a-col> </a-row> <!-- 新增搜索与重置按钮 --> <div class="search-reset-btns" style="margin-top: 15px; display: flex; justify-content: space-between;"> <van-button type="info" block @click="reLoadData">搜索</van-button> <van-button type="default" block @click="handleReset">重置</van-button> </div> </div> <div class="tabBox"> <!-- <van-tabs v-model:active="activeName" color="#1989fa" @change="changeTab">--> <!-- <van-tab title="所有项目" name="0">--> <a-spin :spinning="spinning"> <div class="itemBox" v-for="(item, i) in listData" :key="i" @click="showDetail(item.id)"> <div class="contentBox"> <div class="imgBox"> <template v-if="item.imgUrl"> <van-image :src="item.imgUrl" width="50px" height="50px" style="border-radius: 5px;"> <template v-slot:loading> <van-loading type="spinner" size="20" /> </template> <template v-slot:error>加载失败</template> </van-image> </template> <template v-else> <div style=" width: 50px; height: 50px; display: flex; align-items: center; justify-content: center; background: #f0f0f0; color: #999; font-size: 12px; " > 暂无图片 </div> </template> </div> <div class="msgBox"> <p>{{ item.xmmc ? item.xmmc : '' }}</p> <p>物业服务企业:{{ item.wyfwqy ? item.wyfwqy : '暂无企业' }}</p> <p> 更新时间: {{ item.updateTime ? item.updateTime : '暂无更新时间' }} </p> </div> <!-- <van-icon class="iconBox" name="edit" /> --> </div> <div class="typeBox" v-if="item.fwxz && item.fwxz.length > 0" :class="[ item.fwxz == 1 || item.fwxz == 2 || item.fwxz == 3 || item.fwxz == 4 || item.fwxz == 5 ? 'personalType' : 'publicType' ]" > <span v-for="(item1, i1) in item.fwxz" :key="i1"> <span v-if="item1 == 1">  商品住宅   </span> <span v-if="item1 == 2">  商住两用   </span> <span v-if="item1 == 3">  共有产权   </span> <span v-if="item1 == 4">  保障性住房(经适房、公租房等) )  </span> <span v-if="item1 == 5">  直管公房   </span> <span v-if="item1 == 6">  军产房   </span> <span v-if="item1 == 7">  三供一业   </span> <span v-if="item1 == 8">  非经资产移交   </span> <span v-if="item1 == 9">  房改售房   </span> <span v-if="item1 == 10">  回迁安置房   </span> <span v-if="item1 == -99">  其他   </span> </span> </div> <!-- <div v-else></div> --> </div> <a-button class="moreBtn" @click="getprojectListMsg" :disabled="moreDisabled"> {{ moreText }} <van-icon name="arrow-down" /> </a-button> <!-- 增加一个空div 顶高度--> <div style="height: 100px;width:100%"></div> </a-spin> <!-- </van-tab>--> <!-- <van-tab title="小区列表" name="1">--> <!-- <div style="font-size: 14px">--> <!-- <div class="itemBox" v-for="(item, i) in tab2ListData" :key="i" @click="showDetail(item.id)">--> <!-- <div class="contentBox">--> <!-- <div class="imgBox">--> <!-- <!– <van-image src="" width="50px" height="50px">--> <!-- <template v-slot:loading>--> <!-- <van-loading type="spinner" size="20" />--> <!-- </template>--> <!-- </van-image> –>--> <!-- <template v-if="item.imgUrl">--> <!-- <van-image :src="item.imgUrl" width="50px" height="50px">--> <!-- <template v-slot:loading>--> <!-- <van-loading type="spinner" size="20" />--> <!-- </template>--> <!-- <template v-slot:error>加载失败</template>--> <!-- </van-image>--> <!-- </template>--> <!-- <template v-else>--> <!-- <div--> <!-- style="--> <!-- margin-top: 20px;--> <!-- width: 50px;--> <!-- height: 50px;--> <!-- display: flex;--> <!-- align-items: center;--> <!-- justify-content: center;--> <!-- background: #f0f0f0;--> <!-- color: #999;--> <!-- font-size: 12px;--> <!-- "--> <!-- >--> <!-- 暂无图片--> <!-- </div>--> <!-- </template>--> <!-- </div>--> <!-- <div class="msgBox">--> <!-- <p>{{ item.xmmc ? item.xmmc : '' }}</p>--> <!-- <p>物业服务企业:{{ item.wyfwqy ? item.wyfwqy : '暂无企业' }}</p>--> <!-- <p>--> <!-- 更新时间:--> <!-- {{ item.updateTime ? item.updateTime : '暂无更新时间' }}--> <!-- </p>--> <!-- </div>--> <!-- <!– <van-icon class="iconBox" name="edit" /> –>--> <!-- </div>--> <!-- <div--> <!-- class="typeBox"--> <!-- v-if="item.fwxz"--> <!-- :class="[--> <!-- item.fwxz == 1 || item.fwxz == 2 || item.fwxz == 3 || item.fwxz == 4 || item.fwxz == 5--> <!-- ? 'personalType'--> <!-- : 'publicType'--> <!-- ]"--> <!-- >--> <!-- <span v-for="(item1, i1) in item.fwxz" :key="i1">--> <!-- <span v-if="item1 == 1">  商品住宅   </span>--> <!-- <span v-if="item1 == 2">  商住两用   </span>--> <!-- <span v-if="item1 == 3">  共有产权   </span>--> <!-- <span v-if="item1 == 4">  保障性住房(经适房、公租房等) )  </span>--> <!-- <span v-if="item1 == 5">  直管公房   </span>--> <!-- <span v-if="item1 == 6">  军产房   </span>--> <!-- <span v-if="item1 == 7">  三供一业   </span>--> <!-- <span v-if="item1 == 8">  非经资产移交   </span>--> <!-- <span v-if="item1 == 9">  房改售房   </span>--> <!-- <span v-if="item1 == 10">  回迁安置房   </span>--> <!-- <span v-if="item1 == -99">  其他   </span>--> <!-- </span>--> <!-- </div>--> <!-- <div v-else></div>--> <!-- </div>--> <!-- <a-button class="moreBtn" @click="getTab2List" :disabled="moreDisabled">--> <!-- {{ moreText }}--> <!-- <van-icon name="arrow-down" />--> <!-- </a-button>--> <!-- </div>--> <!-- </van-tab>--> <!-- </van-tabs>--> </div> <!-- <div class="container">--> <!-- <div v-for="(item,i) of notifyList" class="project_list_item">--> <!-- </div>--> <!-- </div>--> <!-- <div class="fixedBox"> 专题 <br />图层 </div> --> <!-- 顶部小区查询项弹框 --> <!-- <van-popup--> <!-- v-model="checkboxGroupShow"--> <!-- position="bottom"--> <!-- :style="{ height: '50%',zIndex:9999 }">--> <!-- <div class="my_checkbox_popup">--> <!-- <p class="cancel" @click="cancel(true)">取消</p>--> <!-- <p class="confirm" @click="comfirm">确认</p>--> <!-- </div>--> <!-- <van-checkbox-group v-model="checkboxGroupIds">--> <!-- <van-cell-group>--> <!-- <van-cell--> <!-- class="my_checkbox_popup_cell"--> <!-- v-for="(items, index) in dataOption"--> <!-- clickable--> <!-- :key="index"--> <!-- :title="items.name"--> <!-- @click="onSelect(items,index)">--> <!-- <template #right-icon>--> <!-- <van-checkbox :name="items.id" ref="CoverableCities" class="my_checkbox_popup_item" />--> <!-- </template>--> <!-- </van-cell>--> <!-- </van-cell-group>--> <!-- </van-checkbox-group>--> <!-- </van-popup>--> </div> </template> <script> import rolemixins from '@/utils/rolemixins'; import { managerListMsg, xioaquList } from '@/api/comm'; import { getSendRangeData } from '@/api/home'; import { getSqmcData } from '@/api/userState'; export default { name: 'projectList', components: {}, mixins: [rolemixins], data() { return { listData: [], tab2ListData: [], activeName: 'tab1', pathStr: this.$route.path, pathName: this.$route.meta.title ? this.$route.meta.title : '', query: {}, //分页 pane: { pageNo: 1, total: 0, pageSize: 10 }, sfwzyhwyValue: false, moreDisabled: false, moreText: '加载更多', streetColumns: [], townColumns: [], spinning: false, timer: null }; }, created() { }, activated() { }, beforeDestroy() { if (this.timer) { clearTimeout(this.timer); } }, mounted() { // this.initCheckboxGroup(); // this.getData(); this.getprojectListMsg(); this.getStreetData(); }, methods: { // 切换tab changeTab(tab) { this.activeName = tab; // this.getTab2List(); console.log('ffffff', tab); this.pane.pageNo = 1; this.moreDisabled = false; this.moreText = '加载更多'; this.pane.total = 0; this.listData = []; this.tab2ListData = []; if (tab == '0') { this.getprojectListMsg(); } else { this.getTab2List(); } }, // 获取数据列表接口 getData() { let params = { id: 1 }; }, // 获取所有项目 async getprojectListMsg() { this.spinning = true; const res = await managerListMsg(this.pane); if (res.success) { this.pane.total = res.result.total; this.listData = [...this.listData, ...res.result.records]; this.listData.forEach((item) => { if (item.fwxz && !Array.isArray(item.fwxz)) { item.fwxz = item.fwxz.split(','); } else { item.fwxz = item.fwxz || []; } }); this.$forceUpdate(); if (this.listData.length >= this.pane.total) { this.moreDisabled = true; this.moreText = '没有更多了'; } else { this.moreDisabled = false; this.moreText = '加载更多'; } } this.spinning = false; }, // 获取小区列表 getTab2List() { xioaquList(this.pane).then((res) => { this.pane.total = res.result.total; this.pane.pageNo = this.pane.pageNo + 1; if (res.code == 200) { if (res.result.records.length < this.pane.pageSize) { this.moreDisabled = true; this.moreText = '没有更多了'; this.tab2ListData = [...this.tab2ListData, ...res.result.records]; this.$forceUpdate(); } else { this.moreDisabled = false; this.moreText = '加载更多'; this.tab2ListData = [...this.tab2ListData, ...res.result.records]; this.$forceUpdate(); } } }); }, // 跳转项目信息详情 filterOption(inputValue, option) { return option.data.attrs.lable ? option.data.attrs.lable.includes(inputValue) : null; }, // 获取街镇数据以及社区数据 getStreetData() { getSendRangeData().then((res) => { this.streetColumns = res.result; }); }, // 获取社区数据 getTownData(val) { getSqmcData({ id: val }).then((res) => { this.townColumns = res.result; }); }, handleTownConfirm(val) { this.sqmcName = val.departName; this.pane.sqmc = val.id; this.townPicker = false; }, handleSearch() { console.log(this.pane); this.pane.pageNo = 1; this.moreDisabled = false; this.moreText = '加载更多'; this.listData = []; this.getprojectListMsg(); }, handleReset() { this.pane = { xmmc: '', szjz: undefined, sqmc: undefined, pageNo: 1, pageSize: 10, total: 0 }; this.moreDisabled = false; this.townColumns = []; this.moreDisabled = false this.moreText = '加载更多'; this.listData = []; this.$nextTick(() => { this.$forceUpdate }) this.getprojectListMsg(); }, showDetail(id) { this.$router.push({ path: '/listDetail', query: { id: id } }); }, changeZyhwy(e) { this.pane.sfwzyhwy = this.sfwzyhwyValue ? 1 : 0; this.reLoadData(); }, handleStreetChange(val) { // 清空社区 this.pane.sqmc = undefined; this.getTownData(val); // this.reLoadData(); }, handleSQChange(val) { this.pane.sqmc = val; // this.reLoadData(); }, reLoadData() { this.pane.pageNo = 1; this.moreDisabled = false; this.moreText = '加载更多'; this.listData = []; this.getprojectListMsg(); }, // changeXmmc(e) { // if (this.timer) { // clearTimeout(this.timer); // } // this.timer = setTimeout(() => { // this.moreDisabled = false; // this.moreText = '加载更多'; // this.listData = []; // // this.getprojectListMsg(); // }, 1000); // } // checkboxGroupShowFn() { // this.checkboxGroupShow = true; // } // initCheckboxGroup() { // this.checkboxGroupIds = []; // this.checkboxGroup = []; // }, // onSelect(items, index) { // this.$refs.CoverableCities[index].toggle(); // }, // comfirm() { // let text = []; // this.dataOption.map((its, i) => { // if (this.checkboxGroupIds.indexOf(its.id) !== -1) { // text.push(its); // } // }); // this.checkboxGroup = text; // this.$forceUpdate(); // this.cancel(false); // }, // cancel(flag) { // this.$forceUpdate(); // this.checkboxGroupShow = false; // if (flag) { // this.initCheckboxGroup(); // } // }, // deleteCheckbox(item) { // this.checkboxGroup.map((its, index) => { // if (item.id === its.id) { // this.checkboxGroup.splice(index, 1); // } // }); // this.checkboxGroupIds = this.checkboxGroup.map(its => { // return its.id; // }); // console.log(this.checkboxGroup, this.checkboxGroupIds, '----deleteCheckbox'); // } } }; </script> <style scoped lang="less"> @import '~@/assets/css/comm.less'; .economicdevelopment-page { height: 100%; .fixedBox { width: 80px; height: 80px; border-radius: 50%; box-shadow: 5px 5px 5px #ccc, -5px -5px 5px #ccc, 5px -5px 5px #ccc, -5px 5px 5px #ccc; position: absolute; bottom: 20px; right: 20px; background: #fff; font-size: 14px; text-align: center; display: flex; align-items: center; justify-content: center; } .top_box { width: auto; height: 40px; background: #fff; border: 1px solid #3b77b3; position: relative; display: flex; align-items: center; padding: 0 5px; margin: 0 10px; .right_icon { position: absolute; top: 5px; right: 10px; } p { padding: 5px 10px 20px 10px; height: 24px; background: rgba(59, 119, 179, 0.15); border-radius: 12px; border: 1px solid rgba(59, 119, 179, 0.5); color: #3b77b3; font-size: 12px; font-weight: 500; font-family: PingFangSC, PingFang SC; margin: 0 2px; span { display: inline-block; margin-right: 10px; } } div.placeholder { width: 100%; color: #d4d3d3; font-size: 16px; font-weight: normal; text-align: center; } } /deep/ .van-tabs__content { overflow-y: scroll; } .tabBox { width: auto; margin: 0 10px; margin-top: 5px; height: 88vh; overflow: hidden; overflow-y: scroll; .itemBox { width: 100%; //height: 80px; position: relative; font-size: 12px; padding: 10px; background: #fff; margin-bottom: 10px; display: flex; align-items: flex-end; margin-top: 5px; .contentBox { display: flex; justify-content: space-between; text-align: center; width: 100%; .iconBox { font-size: 26px; width: auto; height: 30px; line-height: 60px; } .msgBox { text-align: left; width: 200px; } .imgBox { width: 50px; height: 50px; display: flex; align-items: center; justify-content: center; } } .typeBox { position: absolute; height: 20px; line-height: 20px; top: 0; right: 10px; text-align: center; padding: 0 5px; max-width: 50%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .personalType { background: #e99d42; } .publicType { background: #a2ef4d; } } } //.container { // height: 100% !important; // padding: 0 12.5px; // //} } .my_checkbox_popup { width: 100%; height: 45px; line-height: 45px; margin-bottom: 5px; display: flex; justify-content: space-between; align-items: center; p { height: 100%; padding: 0 0.42667rem; font-size: 0.37333rem; background-color: transparent; border: none; cursor: pointer; } p.cancel { color: #969799; } p.confirm { color: #576b95; } } .my_checkbox_popup_cell { /deep/ .van-cell__title { padding: 8px 10px; } } .my_checkbox_popup_item { padding: 10px 15px; } .moreBtn { text-align: center; color: #1989fa; border: none; width: 100%; background: #fff; } .userLoginSearch { background-color: #fff; border-radius: 10px; padding: 10px; margin: 10px 0; .checkboxContainer { width: 100%; border-radius: 10px; border: 1px solid #3b77b3; font-size: 14px; padding: 0 8px; height: 32px; line-height: 32px; display: flex; align-items: center; justify-content: space-between; color: #3b77b3; } } .search-reset-btns { // background-color: #3c78b1; display: flex; gap: 10px; .van-button { background-color: #3c78b1; flex: 1; font-size: 14px; color: #ffffff; } } ::v-deep .ant-select-selection { border-color: #3b77b3; border-radius: 10px; color: #3b77b3; .ant-select-selection__placeholder { color: #3b77b3; } } ::v-deep .ant-input { border-color: #3b77b3; border-radius: 10px; padding-left: 8px; color: #3b77b3; &::placeholder { color: #3b77b3; } } </style> <template v-if="item.imgUrl"> <van-image :src="item.imgUrl" width="50px" height="50px" style="border-radius: 5px;"> <template v-slot:loading> <van-loading type="spinner" size="20" /> </template> <template v-slot:error>加载失败</template> </van-image> </template> <template v-else> <div style=" width: 50px; height: 50px; display: flex; align-items: center; justify-content: center; background: #f0f0f0; color: #999; font-size: 12px; " > 暂无图片 </div> </template>这段代码现在是图片,换成视频,点击视频后可弹出并播放
08-08
<template> <view class="container"> <!-- 视频播放区 --> <view class="video-container"> <video ref="videoPlayer" :muted="liveData.muted" :autoplay="liveData.autoplay" playsinline webkit-playsinline x5-playsinline object-fit="contain" :poster="liveData.poster" class="video" v-show="!hasError" @error="handleVideoError" @play="onPlay" @pause="onPause" @waiting="onBuffering" @touchstart="handleVideoTouch" ></video> <!-- 状态提示 --> <view v-if="isLoadingComponent" class="loading-mask"> <view class="spinner"></view> <text class="loading-text">视频组件加载中...</text> </view> <view v-if="isLoadingPlayer && !hasError" class="loading-mask"> <view class="spinner"></view> <text class="loading-text">直播加载中...{{ loadProgress }}%</text> </view> <view v-if="hasError" class="error-mask"> <text class="error-title">{{ errorTitle }}</text> <text class="error-desc">{{ errorDesc }}</text> <button class="retry-btn" @click="reloadPlayer">重试</button> <button class="switch-btn" @click="switchSource" v-if="!isSingleSource">切换源</button> </view> <!-- 左下角透明聊天功能 --> <view class="chat-container" :class="{ 'chat-expanded': isChatExpanded }"> <view class="chat-header" @click="toggleChatExpand"> <text class="chat-title">直播聊天</text> <text class="chat-toggle-icon">{{ isChatExpanded ? '−' : '+' }}</text> </view> <view class="chat-messages" v-if="isChatExpanded"> <view class="chat-message" v-for="(msg, idx) in chatMessages" :key="idx"> <image class="chat-avatar" :src="msg.avatar || msg.userAvatar" mode="widthFix" ></image> <view class="chat-content-wrap"> <text class="chat-username">{{ msg.username||msg.userNickname }}:</text> <text class="chat-content" :class="{ 'system-msg': msg.type !== 'chat' }"> {{ msg.content }} </text> </view> </view> </view> <view class="chat-input-area"> <input class="chat-input" v-model="chatInput" placeholder="输入消息..." placeholder-style="color: rgba(255,255,255,0.5)" @confirm="sendChatMessage" :disabled="!isChatExpanded" /> <button class="chat-send-btn" @click="sendChatMessage" :disabled="!isChatExpanded || !chatInput.trim()" > <!-- || wsStatus !== 'open' 需要在disabled里面添加这个实时通信 --> 发送 </button> </view> </view> </view> </view> </template> <script> import Hls from 'hls.js'; import { livecomment,liveroomlike } from '@/api/graphic.js'; export default { data() { return { queryliat:{//直播间评论 roomId:80, userId:null, content:null, }, statusBarHeight: 0, isUsingCustomSource: true, isSingleSource: false, sources: { test: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8', custom: 'http://139.159.156.31:8082/live/test_stream.m3u8', appBackup: 'https://backup-cdn.com/live/app_stream.m3u8' }, liveData: { muted: false, autoplay: true, poster: 'https://picsum.photos/800/450' }, isLoadingComponent: true, isLoadingPlayer: false, isBuffering: false, hasError: false, errorTitle: '', errorDesc: '', loadProgress: 0, initRetryCount: 0, maxInitRetry: 3, hls: null, isNativeHLS: false, isPlaying: false, // 聊天功能相关 isChatExpanded: false, chatInput: '', chatMessages: [ { username: '系统', content: '欢迎进入直播聊天室~', type: 'system', avatar: '' } ], // 实时通信相关 ws: null, wsUrl: 'ws://192.168.1.4:8081/ws/live/comment', // 替换为实际WebSocket服务地址 wsStatus: 'closed', // closed/connecting/open/error reconnectCount: 0, maxReconnect: 5, roomId: '', userInfo: { username: uni.getStorageSync('username') || '观众' + Math.floor(Math.random() * 1000), avatar: uni.getStorageSync('avatar') || 'https://picsum.photos/100/100?random=' + Math.floor(Math.random() * 100) } }; }, computed: { currentSource() { return this.isUsingCustomSource ? this.sources.custom : this.sources.test; } }, onLoad(options) { console.log('接收的直播参数:', options); // 初始化直播间ID(用于隔离聊天房间) this.roomId = options.roomId || options.id || 'defaultLiveRoom'; if (options.id) this.sources.custom = options.id; const systemInfo = uni.getSystemInfoSync(); this.statusBarHeight = systemInfo.statusBarHeight || 0; this.checkCustomSourceAvailability(); setTimeout(() => { this.$nextTick(() => { this.initVideoComponent(); this.initWebSocket(); // 初始化实时聊天 }); }, 500); }, onUnload() { this.destroyPlayer(); // 关闭WebSocket连接 if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.sendWsMessage({ type: 'leave', content: '离开了直播间' }); setTimeout(() => { this.ws.close(1000, '用户离开'); }, 500); } }, methods: { checkCustomSourceAvailability() { // 移除XMLHttpRequest相关代码,改用uni.request uni.request({ url: this.sources.custom, method: 'HEAD', // 发送HEAD请求检测源是否可用 timeout: 3000, // 超时时间3秒 success: (res) => { // 状态码200-399表示源可访问 if (res.statusCode >= 200 && res.statusCode < 400) { console.log('自定义源可访问'); } else { console.warn('自定义源不可访问,切换到测试源'); this.isUsingCustomSource = false; this.isSingleSource = false; } }, fail: (err) => { console.error('自定义源请求失败,切换到测试源', err); this.isUsingCustomSource = false; this.isSingleSource = false; } }); }, initVideoComponent() { const uniVideoRef = this.$refs.videoPlayer; if (!uniVideoRef) { this.retryInit('$refs.videoPlayer 未找到(组件未渲染)'); return; } // 第步:检查$el是否存在(部分场景下组件可能已卸载) if (!uniVideoRef.$el) { this.retryInit('video组件的$el不存在(组件已卸载)'); return; } // 第三步:安全获取video元素 const videoEl = uniVideoRef.$el.querySelector('video'); if (!videoEl || videoEl.tagName !== 'VIDEO') { this.retryInit('未提取到原生video标签'); return; } this.isLoadingComponent = false; this.checkNativeHLSSupport(videoEl); this.initPlayer(videoEl); }, retryInit(reason) { console.warn(`初始化重试(${this.initRetryCount + 1}/${this.maxInitRetry}):${reason}`); if (this.initRetryCount < this.maxInitRetry) { this.initRetryCount++; setTimeout(() => this.initVideoComponent(), 1000); return; } this.isLoadingComponent = false; this.setError('组件初始化失败', reason + ',请重启项目或检查设备'); }, // 修改checkNativeHLSSupport方法,增加更严格的格式检测 checkNativeHLSSupport(videoEl) { try { // 增加HLS格式白名单检测 const hlsTypes = ['application/vnd.apple.mpegurl', 'application/x-mpegURL']; const supportLevel = hlsTypes.map(type => videoEl.canPlayType(type)) .find(level => level === 'probably' || level === 'maybe'); this.isNativeHLS = !!supportLevel; console.log('严格HLS支持检测:', this.isNativeHLS); } catch (err) { this.isNativeHLS = false; console.error('HLS支持检测异常:', err); } }, initPlayer(videoEl) { this.isLoadingPlayer = true; // App端特殊处理 if (uni.getSystemInfoSync().platform !== 'h5') { // 先静音播放(绕过自动播放限制) videoEl.muted = true; this.liveData.muted = true; // 增加用户交互后恢复声音 const restoreAudio = () => { videoEl.muted = false; this.liveData.muted = false; document.removeEventListener('click', restoreAudio); }; document.addEventListener('click', restoreAudio, { once: true }); } if (this.isNativeHLS) { this.playWithNative(videoEl); return; } this.playWithHLS(videoEl); }, playWithNative(videoEl) { videoEl.src = this.currentSource; videoEl.addEventListener('progress', () => { if (videoEl.buffered.length > 0) { const loadedEnd = videoEl.buffered.end(videoEl.buffered.length - 1); this.loadProgress = Math.floor((loadedEnd / (videoEl.duration || 1)) * 100) || 0; } }); videoEl.play() .then(() => { this.isLoadingPlayer = false; this.isPlaying = true; }) .catch(err => this.setError('原生播放失败', err.message || '自动播放被拦截')); }, playWithHLS(videoEl) { if (uni.getSystemInfoSync().platform === 'ios' || uni.getSystemInfoSync().platform === 'android') { // App端强制使用原生播放(如果可能) if (this.isNativeHLS) { this.playWithNative(videoEl); return; } // 增加App端HLS.js配置 this.hls = new Hls({ // App端需要关闭的配置项 enableWorker: false, lowLatencyMode: true, // 增加App端专用配置 appSpecific: { useNativeHLSWhenAvailable: true } }); } if (typeof Hls === 'undefined') { this.setError('依赖缺失', '请安装HLS.js(npm install hls.js)'); this.isLoadingPlayer = false; return; } if (this.hls) { this.hls.destroy(); this.hls = null; } this.hls = new Hls({ maxBufferLength: 20, maxMaxBufferLength: 30, startLevel: 0, enableWorker: false, lowLatencyMode: false, fragLoadTimeOut: 30000, fragMaxRetry: 3 }); this.hls.attachMedia(videoEl); this.hls.on(Hls.Events.ERROR, (_, data) => { console.error('HLS.js错误:', data); if (data.fatal) { const errMap = { networkError: '网络错误(检查网络或源地址)', mediaError: '格式不兼容(需H.264+AAC编码)', manifestError: 'm3u8清单格式错误' }; this.setError(errMap[data.type] || 'HLS播放失败', data.details || '未知错误'); this.isSingleSource = Object.keys(this.sources).length <= 1; this.isLoadingPlayer = false; } }); this.hls.loadSource(this.currentSource); this.hls.on(Hls.Events.MANIFEST_PARSED, () => { console.log('HLS清单解析成功'); this.isLoadingPlayer = false; videoEl.play().then(() => { this.isPlaying = true; }).catch(err => { this.setError('播放失败', err.name === 'NotAllowedError' ? '自动播放被拦截(点击视频开启)' : err.message); }); }); this.hls.on(Hls.Events.BUFFER_LEVEL_STATE_CHANGED, (_, data) => { if (data.level === -1) return; this.loadProgress = Math.floor((data.bufferLength / (data.targetDuration || 1)) * 100) || 0; }); }, switchSource() { this.isUsingCustomSource = !this.isUsingCustomSource; this.destroyPlayer(); this.$nextTick(() => { this.isLoadingComponent = true; this.initVideoComponent(); }); }, reloadPlayer() { this.hasError = false; this.isLoadingComponent = true; this.initRetryCount = 0; this.destroyPlayer(); this.$nextTick(() => { this.initVideoComponent(); }); }, destroyPlayer() { if (this.hls) { this.hls.destroy(); this.hls = null; } const uniVideoRef = this.$refs.videoPlayer; if (uniVideoRef) { const videoEl = uniVideoRef.$el.querySelector('video'); if (videoEl) { videoEl.pause(); videoEl.src = ''; videoEl.load(); } } this.isLoadingPlayer = false; this.loadProgress = 0; this.isPlaying = false; }, setError(title, desc) { this.hasError = true; this.errorTitle = title; this.errorDesc = desc; this.isLoadingPlayer = false; this.isLoadingComponent = false; }, handleVideoError(err) { const errDetail = err.detail || {}; if (errDetail.errCode || errDetail.errMsg) { console.error('uni-video错误:', errDetail); this.setError('视频组件错误', `错误码: ${errDetail.errCode || '未知'}, 信息: ${errDetail.errMsg || '无'}`); } }, toggleMute() { this.liveData.muted = !this.liveData.muted; const uniVideoRef = this.$refs.videoPlayer; if (uniVideoRef) { const videoEl = uniVideoRef.$el.querySelector('video'); if (videoEl) videoEl.muted = this.liveData.muted; } }, onPlay() { this.isBuffering = false; this.isPlaying = true; }, onPause() { if (!this.hasError) this.isBuffering = true; this.isPlaying = false; }, onBuffering() { this.isBuffering = true; }, // 聊天功能方法 toggleChatExpand() { this.isChatExpanded = !this.isChatExpanded; this.$nextTick(() => this.scrollToChatBottom()); }, sendChatMessage() { const userlist = uni.getStorageSync('UID'); const datalist = uni.getStorageSync('USER_INFO'); const userInfo = JSON.parse(datalist); this.queryliat.userId=userlist const content = this.chatInput.trim(); this.queryliat.content=content if (!content) return; this.chatInput = ''; this.scrollToChatBottom(); livecomment(80,userlist,content).then(res => { console.log(res,'asdas') }); // 发送到WebSocket后端 this.sendWsMessage({ content: content }); }, scrollToChatBottom() { if (!this.isChatExpanded) return; const msgContainer = uni.createSelectorQuery().in(this).select('.chat-messages'); msgContainer.fields({ scrollOffset: true, size: true }, res => { if (res) { msgContainer.scrollOffset({ scrollTop: res.scrollHeight, duration: 300 }); } }).exec(); }, // 实时通信方法 initWebSocket() { // 避免重复连接 if (this.wsStatus === 'connecting' || this.wsStatus === 'open') return; // 1. 验证WebSocket URL格式(防止URL错误) if (!this.wsUrl.startsWith('ws://') && !this.wsUrl.startsWith('wss://')) { console.error('WebSocket URL格式错误,必须以ws://或wss://开头'); this.showToast('聊天服务地址错误'); return; } // 拼接完整URL(固定roomId为80,确保参数正确) const wsUrlWithRoom = `${this.wsUrl}/80`; console.log('尝试连接WebSocket:', wsUrlWithRoom); this.wsStatus = 'connecting'; // 2. 增加连接超时处理(防止一直卡在connecting状态) const connectionTimeout = setTimeout(() => { if (this.wsStatus === 'connecting') { console.error('WebSocket连接超时(10秒未响应)'); this.wsStatus = 'error'; this.showToast('聊天连接超时'); this.destroyWebSocket(); // 主动销毁超时连接 this.reconnectWebSocket(); } }, 10000); // 10秒超时 try { this.ws = new WebSocket(wsUrlWithRoom); // 连接成功 this.ws.onopen = () => { clearTimeout(connectionTimeout); // 清除超时器 console.log('WebSocket连接成功'); this.wsStatus = 'open'; this.reconnectCount = 0; // 重置重连计数 this.sendWsMessage({ type: 'enter', content: '进入了直播间' }); }; // 接收消息(保持不变) this.ws.onmessage = (event) => { try { const msg = JSON.parse(event.data); if (msg.username === this.userInfo.username && msg.type !== 'enter') return; console.log(msg.data,'获取消息') this.chatMessages.push(msg.data); this.$nextTick(() => this.scrollToChatBottom()); } catch (err) { console.error('解析WebSocket消息失败:', err); } }; // 3. 增强错误信息输出(关键修改) this.ws.onerror = (event) => { clearTimeout(connectionTimeout); // 打印完整错误上下文 console.error('WebSocket错误详情:'); console.error('连接URL:', wsUrlWithRoom); console.error('错误事件:', event); console.error('WebSocket状态:', this.ws?.readyState ? ['连接中', '已连接', '关闭中', '已关闭'][this.ws.readyState] : '未初始化'); // 根据状态提示可能的原因 let errorMsg = '聊天连接失败'; if (this.ws?.readyState === 3) { // 已关闭状态 errorMsg += '(服务端拒绝连接或地址错误)'; } else if (this.ws?.readyState === 0) { // 连接中出错 errorMsg += '(网络异常或服务未启动)'; } this.wsStatus = 'error'; this.showToast(errorMsg); this.reconnectWebSocket(); }; // 4. 处理关闭事件(根据关闭代码判断原因) this.ws.onclose = (event) => { clearTimeout(connectionTimeout); console.log(`WebSocket关闭 - 代码: ${event.code}, 原因: ${event.reason || '无'}`); this.wsStatus = 'closed'; // 常见关闭代码含义: // 1000: 正常关闭;1006: 异常关闭(服务端未启动/网络断连);1011: 服务端错误 const shouldReconnect = [1006, 1011, 1012].includes(event.code) && this.reconnectCount < this.maxReconnect; if (shouldReconnect) { this.reconnectWebSocket(); } else if (event.code !== 1000) { this.showToast(`聊天连接已关闭(代码: ${event.code})`); } }; } catch (err) { clearTimeout(connectionTimeout); console.error('创建WebSocket实例失败:', err); this.wsStatus = 'error'; // 捕获URL格式错误等初始化异常 this.showToast('聊天服务初始化失败(可能是地址格式错误)'); this.reconnectWebSocket(); } }, // 新增:主动销毁WebSocket连接的方法 destroyWebSocket() { if (this.ws) { try { this.ws.close(1000, '主动关闭'); } catch (err) { console.error('关闭WebSocket失败:', err); } this.ws = null; } }, // 优化:重连机制(指数退避策略) reconnectWebSocket() { if (this.reconnectCount >= this.maxReconnect) { console.error(`已达最大重连次数(${this.maxReconnect}次),停止重连`); this.showToast('多次连接失败,请检查服务状态'); return; } // 指数退避:1s → 2s → 4s → 8s(最多8秒) const delay = Math.min(1000 * Math.pow(2, this.reconnectCount), 8000); this.reconnectCount++; console.log(`第${this.reconnectCount}次重连(延迟${delay}ms)`); setTimeout(() => { this.destroyWebSocket(); // 先销毁旧连接 this.initWebSocket(); }, delay); }, reconnectWebSocket() { if (this.reconnectCount >= this.maxReconnect) { this.showToast('聊天连接失败,请刷新页面'); return; } const delay = (this.reconnectCount + 1) * 2000; setTimeout(() => { this.reconnectCount++; console.log(`第${this.reconnectCount}次重连`); this.initWebSocket(); }, delay); }, sendWsMessage(msgData) { if (this.wsStatus !== 'open' || !this.ws) { this.showToast('聊天连接中,请稍后'); return; } const msg = { username: this.userInfo.username, avatar: this.userInfo.avatar, content: '', type: 'chat', timestamp: new Date().getTime(), ...msgData }; try { this.ws.send(JSON.stringify(msg)); } catch (err) { console.error('发送消息失败:', err); this.showToast('消息发送失败'); } }, showToast(title) { uni.showToast({ title, icon: 'none', duration: 1500 }); } } }; </script> <style scoped> .container { width: 100%; height: 100vh; background-color: #000; display: flex; flex-direction: column; overflow: hidden; } .video-container { flex: 1; position: relative; width: 100%; } /* 隐藏视频控件 */ ::v-deep video::-webkit-media-controls { display: none !important; } ::v-deep video::-moz-media-controls { display: none !important; } ::v-deep video::-ms-media-controls { display: none !important; } ::v-deep video::-o-media-controls { display: none !important; } ::v-deep video::-webkit-media-controls-enclosure { display: none !important; } ::v-deep video::-webkit-media-controls-panel { display: none !important; } .video { width: 100%; height: 100%; pointer-events: none; } /* 状态提示样式 */ .loading-mask { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.7); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 5; } .spinner { width: 80rpx; height: 80rpx; border: 8rpx solid rgba(255, 255, 255, 0.2); border-top-color: #fff; border-radius: 50%; animation: spin 1s linear infinite; } .loading-text { color: #fff; font-size: 28rpx; margin-top: 20rpx; } .error-mask { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.7); display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40rpx; gap: 20rpx; z-index: 5; } .error-title { color: #ff4d4f; font-size: 32rpx; text-align: center; } .error-desc { color: #fff; font-size: 26rpx; text-align: center; line-height: 1.5; } .retry-btn, .switch-btn { background-color: #1677ff; color: #fff; border-radius: 30rpx; padding: 15rpx 60rpx; font-size: 28rpx; width: auto; margin-top: 10rpx; } .switch-btn { background-color: #4CAF50; } /* 聊天功能样式 */ .chat-container { position: absolute; bottom: 0rpx; left: 5rpx; width: calc(100% - 20rpx); background-color: rgba(0, 0, 0, 0.5); border-radius: 20rpx; overflow: hidden; z-index: 4; transition: height 0.3s ease; } .chat-header { display: flex; justify-content: space-between; align-items: center; padding: 15rpx 20rpx; background-color: rgba(0, 0, 0, 0.6); } .chat-title { color: #fff; font-size: 28rpx; font-weight: 500; } .chat-toggle-icon { color: #fff; font-size: 32rpx; font-weight: bold; } .chat-messages { height: 300rpx; padding: 15rpx 20rpx; overflow-y: auto; display: none; } .chat-expanded .chat-messages { display: block; } .chat-message { display: flex; align-items: flex-start; margin-bottom: 15rpx; line-height: 1.5; } .chat-avatar { width: 40rpx; height: 40rpx; border-radius: 50%; margin-right: 10rpx; margin-top: 5rpx; } .chat-content-wrap { display: flex; flex-direction: column; } .chat-username { color: #40a9ff; font-size: 26rpx; margin-right: 10rpx; font-weight: 500; } .chat-content { color: #fff; font-size: 26rpx; } .system-msg { color: #b3b3b3; font-style: italic; } .chat-input-area { display: flex; align-items: center; padding: 15rpx 20rpx; border-top: 1px solid rgba(255, 255, 255, 0.1); } .chat-input { flex: 1; height: 50rpx; color: #fff; font-size: 26rpx; background-color: rgba(255, 255, 255, 0.1); border-radius: 30rpx; padding: 0 20rpx; margin-right: 15rpx; } .chat-send-btn { background-color: #1677ff; color: #fff; font-size: 26rpx; border-radius: 30rpx; padding: 10rpx 30rpx; } .chat-send-btn:disabled { background-color: rgba(255, 255, 255, 0.3); color: rgba(255, 255, 255, 0.6); } /* 动画和滚动条 */ @keyframes spin { to { transform: rotate(360deg); } } .chat-messages::-webkit-scrollbar { width: 8rpx; } .chat-messages::-webkit-scrollbar-track { background-color: rgba(255, 255, 255, 0.1); border-radius: 10rpx; } .chat-messages::-webkit-scrollbar-thumb { background-color: rgba(255, 255, 255, 0.3); border-radius: 10rpx; } </style>把这个代码兼容app播放
09-10
分析mreditor.vue及mrdncrud中病態編輯的功能為範例,新加入一個辨證編輯的功能,修改mreditor.vue代碼,並增加一個mrdiancrud.vue mreditor.vue: <template> <div> <div> <div class="wangeditor"> <WangEditor v-model="content" @response="(msg) => content = msg" /> </div> <div class="up-panel"> <div class="tag-panel"> <mrdncrud v-if="showDnCrud" @tag-updated="handleDnTagUpdated" /> <mrmncrud v-if="showMnCrud" @tag-updated="handleMnTagUpdated" /> <mrsncrud v-if="showSnCrud" @tag-updated="handleSnTagUpdated" /> <mrpncrud v-if="showPnCrud" @tag-updated="handlePnTagUpdated" /> <mrfncrud v-if="showFnCrud" @tag-updated="handleFnTagUpdated" /> </div> <div class="crud-buttons"> <button @click="toggleDnCrud" class="fixed-dncrud-btn"> {{ showDnCrud ? '病態編輯' : '病態編輯' }} </button> <button @click="toggleMnCrud" class="fixed-mncrud-btn"> {{ showMnCrud ? '本草編輯' : '本草編輯' }} </button> <button @click="toggleSnCrud" class="fixed-sncrud-btn"> {{ showSnCrud ? '典籍編輯' : '典籍編輯' }} </button> <button @click="togglePnCrud" class="fixed-pncrud-btn"> {{ showPnCrud ? '方劑編輯' : '方劑編輯' }} </button> <button @click="toggleFnCrud" class="fixed-fncrud-btn"> {{ showFnCrud ? '人物編輯' : '人物編輯' }} </button> </div> </div> <div class="right-panel"> <div class="dntag-display" v-if="currentDnTags.length"> <div class="field-name">相關病態:</div> <div class="dntag-value"> <span v-for="(tag, index) in currentDnTags" :key="'dn-'+index" class="dn-tag" style="color: rgb(212, 107, 8);"> {{ tag }}<span v-if="index < currentDnTags.length - 1">;</span> </span> </div> </div> <div class="dntag-display" v-else> <div class="field-name">相關病態:</div> <div class="dntag-value">無關聯病態名稱</div> </div> <div class="mntag-display" v-if="currentMnTags.length"> <div class="field-name">相關本草:</div> <div class="mntag-value"> <span v-for="(tag, index) in currentMnTags" :key="'mn-'+index" class="mn-tag" style="color: rgb(0, 128, 0);"> {{ tag }}<span v-if="index < currentMnTags.length - 1">;</span> </span> </div> </div> <div class="mntag-display" v-else> <div class="field-name">相關本草:</div> <div class="mntag-value">無關聯本草名稱</div> </div> <div class="sntag-display" v-if="currentSnTags.length"> <div class="field-name">相關典籍:</div> <div class="sntag-value"> <span v-for="(tag, index) in currentSnTags" :key="'sn-'+index" class="sn-tag" style="color: rgb(51, 102, 255);"> {{ tag }}<span v-if="index < currentSnTags.length - 1">;</span> </span> </div> </div> <div class="sntag-display" v-else> <div class="field-name">相關典籍:</div> <div class="sntag-value">無關聯典籍名稱</div> </div> <div class="pntag-display" v-if="currentPnTags.length"> <div class="field-name">相關方劑:</div> <div class="pntag-value"> <span v-for="(tag, index) in currentPnTags" :key="'pn-'+index" class="pn-tag" style="color: rgb(138, 43, 226);"> {{ tag }}<span v-if="index < currentPnTags.length - 1">;</span> </span> </div> </div> <div class="pntag-display" v-else> <div class="field-name">相關方劑:</div> <div class="pntag-value">無關聯方劑名稱</div> </div> <div class="fntag-display" v-if="currentFnTags.length"> <div class="field-name">相關人物:</div> <div class="fntag-value"> <span v-for="(tag, index) in currentFnTags" :key="'fn-'+index" class="fn-tag" style="color: rgb(139, 69, 19);"> {{ tag }}<span v-if="index < currentFnTags.length - 1">;</span> </span> </div> </div> <div class="fntag-display" v-else> <div class="field-name">相關人物:</div> <div class="fntag-value">無關聯人物名稱 </div> </div> </div> </div> <div class="content-display" v-html="displayContent"></div> <footer class="sticky-footer"> <!-- 修改点:将reset按钮绑定到confirmReset方法 --> <span><button @click="confirmReset" class="reset-btn">輸入新醫案</button></span> <span class="form-input"> <input v-model="formData.mrname" placeholder="醫案命名" class="name-input" /> </span> <span class="form-input"> <input v-model="formData.mrorigin" placeholder="醫案出處" class="origin-input" /> </span> <span class="form-input"> <input v-model="formData.mrdoctor" placeholder="醫案醫者" class="doctor-input" /> </span> <span class="form-input"> <input v-model="formData.mrposter" placeholder="提交者姓名" class="poster-input" /> </span> <span><button @click="toggleDisplayFormat">{{ displayButtonText }}</button></span> <span class="id-input"> <input v-model="fetchId" placeholder="輸入醫案ID" type="number" min="1" /> <button @click="fetchById">載入醫案</button> </span> <span v-if="submittedId"> <button @click="updateContent" class="update-btn">更新醫案</button> <button @click="deleteContent" class="delete-btn">刪除醫案</button> </span> <span v-else> <button @click="submitContent" class="submit-btn">提交醫案</button> </span> <span v-if="submittedId" class="submitted-id">醫案 ID: {{ submittedId }}</span> <span>醫案編輯器</span> </footer> </div> </template> <script> import WangEditor from './WangEditor.vue'; import mrdncrud from './mrdncrud.vue'; import mrmncrud from './mrmncrud.vue'; import mrsncrud from './mrsncrud.vue'; import mrpncrud from './mrpncrud.vue'; import mrfncrud from './mrfncrud.vue'; import LZString from 'lz-string'; export default { name: 'mreditor', components: { WangEditor, mrdncrud, mrmncrud, mrsncrud, mrpncrud, mrfncrud }, data() { return { content: '', submittedId: null, fetchId: null, dnTags: [], mnTags: [], snTags: [], pnTags: [], fnTags: [], showRawHtml: false, stateVersion: '1.5', autoSaveTimer: null, autoSaveInterval: 30000, currentDnTags: [], currentMnTags: [], currentSnTags: [], currentPnTags: [], currentFnTags: [], dnTagMap: {}, mnTagMap: {}, snTagMap: {}, pnTagMap: {}, fnTagMap: {}, showDnCrud: false, showMnCrud: false, showSnCrud: false, showPnCrud: false, showFnCrud: false, formData: { mrname: "", mrorigin: "", mrdoctor: "", mrposter: "" } }; }, computed: { highlightedContent() { if (!this.dnTags.length && !this.mnTags.length && !this.snTags.length && !this.pnTags.length && !this.fnTags.length) return this.content; const tempEl = document.createElement('div'); tempEl.innerHTML = this.content; if (this.dnTags.length) { const walker = document.createTreeWalker( tempEl, NodeFilter.SHOW_TEXT ); const nodes = []; while (walker.nextNode()) { nodes.push(walker.currentNode); } nodes.forEach(node => { let text = node.nodeValue; let newHtml = text; this.dnTags .slice() .sort((a, b) => b.length - a.length) .forEach(tag => { const regex = new RegExp( this.escapeRegExp(tag), 'g' ); newHtml = newHtml.replace( regex, `<span style="color: rgb(212, 107, 8); font-weight: bold;">${tag}</span>` ); }); if (newHtml !== text) { const span = document.createElement('span'); span.innerHTML = newHtml; node.parentNode.replaceChild(span, node); } }); } if (this.mnTags.length) { const mnWalker = document.createTreeWalker( tempEl, NodeFilter.SHOW_TEXT ); const mnNodes = []; while (mnWalker.nextNode()) mnNodes.push(mnWalker.currentNode); mnNodes.forEach(node => { let text = node.nodeValue; let newHtml = text; this.mnTags .slice() .sort((a, b) => b.length - a.length) .forEach(tag => { const regex = new RegExp(this.escapeRegExp(tag), 'g'); newHtml = newHtml.replace( regex, `<span style="color: rgb(0, 128, 0); font-weight: bold;">${tag}</span>` ); }); if (newHtml !== text) { const span = document.createElement('span'); span.innerHTML = newHtml; node.parentNode.replaceChild(span, node); } }); } if (this.snTags.length) { const snWalker = document.createTreeWalker( tempEl, NodeFilter.SHOW_TEXT ); const snNodes = []; while (snWalker.nextNode()) snNodes.push(snWalker.currentNode); snNodes.forEach(node => { let text = node.nodeValue; let newHtml = text; this.snTags .slice() .sort((a, b) => b.length - a.length) .forEach(tag => { const regex = new RegExp(this.escapeRegExp(tag), 'g'); newHtml = newHtml.replace( regex, `<span style="color: rgb(51, 102, 255); font-weight: bold;">${tag}</span>` ); }); if (newHtml !== text) { const span = document.createElement('span'); span.innerHTML = newHtml; node.parentNode.replaceChild(span, node); } }); } if (this.pnTags.length) { const pnWalker = document.createTreeWalker( tempEl, NodeFilter.SHOW_TEXT ); const pnNodes = []; while (pnWalker.nextNode()) pnNodes.push(pnWalker.currentNode); pnNodes.forEach(node => { let text = node.nodeValue; let newHtml = text; this.pnTags .slice() .sort((a, b) => b.length - a.length) .forEach(tag => { const regex = new RegExp(this.escapeRegExp(tag), 'g'); newHtml = newHtml.replace( regex, `<span style="color: rgb(138, 43, 226); font-weight: bold;">${tag}</span>` ); }); if (newHtml !== text) { const span = document.createElement('span'); span.innerHTML = newHtml; node.parentNode.replaceChild(span, node); } }); } if (this.fnTags.length) { const fnWalker = document.createTreeWalker( tempEl, NodeFilter.SHOW_TEXT ); const fnNodes = []; while (fnWalker.nextNode()) fnNodes.push(fnWalker.currentNode); fnNodes.forEach(node => { let text = node.nodeValue; let newHtml = text; this.fnTags .slice() .sort((a, b) => b.length - a.length) .forEach(tag => { const regex = new RegExp(this.escapeRegExp(tag), 'g'); newHtml = newHtml.replace( regex, `<span style="color: rgb(139, 69, 19); font-weight: bold;">${tag}</span>` ); }); if (newHtml !== text) { const span = document.createElement('span'); span.innerHTML = newHtml; node.parentNode.replaceChild(span, node); } }); } return tempEl.innerHTML; }, displayContent() { if (this.showRawHtml) { return this.escapeHtml(this.content); } return this.highlightedContent; }, displayButtonText() { return this.showRawHtml ? '病態渲染格式' : '病態原始標籤'; } }, watch: { content: { handler() { this.debouncedSaveState(); }, deep: true } }, async mounted() { await this.fetchDNTags(); await this.fetchMNTags(); await this.fetchSNTags(); await this.fetchPNTags(); await this.fetchFNTags(); this.restoreState(); this.setupAutoSave(); }, activated() { console.log('醫案編輯器被激活'); this.restoreState(); this.setupAutoSave(); }, deactivated() { console.log('醫案編輯器被停用'); this.saveState(); this.clearAutoSave(); }, beforeDestroy() { this.clearAutoSave(); }, methods: { // 新增方法:确认重置逻辑 confirmReset() { // 检查是否有需要保存的数据 const hasData = this.content || this.submittedId || this.formData.mrname || this.formData.mrorigin || this.formData.mrdoctor || this.formData.mrposter; if (hasData) { // 显示警告对话框 if (confirm('輸入欄位中有資料!回原頁面繼續更新檔案請按確定,離開原頁面請按取消。')) { // 用户选择"是":回到原来页面(不执行任何操作) return; } else { // 用户选择"否":清除数据并重置 this.resetAll(); } } else { // 没有数据时直接重置 this.resetAll(); } }, // 重置所有数据 resetAll() { this.content = ''; this.submittedId = null; this.fetchId = null; this.showRawHtml = false; this.currentDnTags = []; this.currentMnTags = []; this.currentSnTags = []; this.currentPnTags = []; this.currentFnTags = []; this.showDnCrud = false; this.showMnCrud = false; this.showSnCrud = false; this.showPnCrud = false; this.showFnCrud = false; this.formData = { mrname: "", mrorigin: "", mrdoctor: "", mrposter: "" }; // 清除自动保存状态 this.clearState(); // 关闭标签面板 const panel = document.querySelector('.tag-panel'); if (panel) panel.style.display = 'none'; }, toggleDnCrud() { this.showDnCrud = !this.showDnCrud; this.showMnCrud = false; this.showSnCrud = false; this.showPnCrud = false; this.showFnCrud = false; this.updatePanelDisplay(); }, toggleMnCrud() { this.showMnCrud = !this.showMnCrud; this.showDnCrud = false; this.showSnCrud = false; this.showPnCrud = false; this.showFnCrud = false; this.updatePanelDisplay(); }, toggleSnCrud() { this.showSnCrud = !this.showSnCrud; this.showDnCrud = false; this.showMnCrud = false; this.showPnCrud = false; this.showFnCrud = false; this.updatePanelDisplay(); }, togglePnCrud() { this.showPnCrud = !this.showPnCrud; this.showDnCrud = false; this.showMnCrud = false; this.showSnCrud = false; this.showFnCrud = false; this.updatePanelDisplay(); }, toggleFnCrud() { this.showFnCrud = !this.showFnCrud; this.showDnCrud = false; this.showMnCrud = false; this.showSnCrud = false; this.showPnCrud = false; this.updatePanelDisplay(); }, updatePanelDisplay() { const panel = document.querySelector('.tag-panel'); if (this.showDnCrud || this.showMnCrud || this.showSnCrud || this.showPnCrud || this.showFnCrud) { panel.style.display = 'block'; } else { panel.style.display = 'none'; } }, handleDnTagUpdated() { console.log('病态标签已更新,刷新数据'); this.fetchDNTags(); }, handleMnTagUpdated() { console.log('本草标签已更新,刷新数据'); this.fetchMNTags(); }, handleSnTagUpdated() { console.log('典籍标签已更新,刷新数据'); this.fetchSNTags(); }, handlePnTagUpdated() { console.log('方剂标签已更新,刷新数据'); this.fetchPNTags(); }, handleFnTagUpdated() { console.log('人物标签已更新,刷新数据'); this.fetchFNTags(); }, async fetchPNTags() { try { const response = await fetch('PNTag/?format=json'); const data = await response.json(); this.pnTags = data .map(item => item.pnname) .filter(name => name && name.trim().length > 0); this.pnTagMap = {}; data.forEach(item => { if (item.id && item.pnname) { this.pnTagMap[item.id] = item.pnname; } }); } catch (error) { console.error('获取方剂标签失败:', error); alert('方剂标签数据加载失败,高亮功能不可用'); } }, async fetchSNTags() { try { const response = await fetch('SNTag/?format=json'); const data = await response.json(); this.snTags = data .map(item => item.snname) .filter(name => name && name.trim().length > 0); this.snTagMap = {}; data.forEach(item => { if (item.id && item.snname) { this.snTagMap[item.id] = item.snname; } }); } catch (error) { console.error('获取典籍标签失败:', error); alert('典籍标签数据加载失败,高亮功能不可用'); } }, async fetchFNTags() { try { const response = await fetch('FNTag/?format=json'); const data = await response.json(); this.fnTags = data .map(item => item.fnname) .filter(name => name && name.trim().length > 0); this.fnTagMap = {}; data.forEach(item => { if (item.id && item.fnname) { this.fnTagMap[item.id] = item.fnname; } }); } catch (error) { console.error('获取人物标签失败:', error); alert('人物标签数据加载失败,高亮功能不可用'); } }, escapeHtml(unsafe) { if (!unsafe) return ''; return unsafe .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, """) .replace(/'/g, "'"); }, escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }, setupAutoSave() { this.clearAutoSave(); this.autoSaveTimer = setInterval(() => { if (this.content || this.submittedId) { console.log('自动保存状态...'); this.saveState(); } }, this.autoSaveInterval); }, clearAutoSave() { if (this.autoSaveTimer) { clearInterval(this.autoSaveTimer); this.autoSaveTimer = null; } }, debouncedSaveState() { this.clearAutoSave(); this.autoSaveTimer = setTimeout(() => { this.saveState(); this.setupAutoSave(); }, 2000); }, saveState() { const state = { version: this.stateVersion, content: this.content, submittedId: this.submittedId, formData: this.formData, showRawHtml: this.showRawHtml, currentDnTags: this.currentDnTags, currentMnTags: this.currentMnTags, currentSnTags: this.currentSnTags, currentPnTags: this.currentPnTags, currentFnTags: this.currentFnTags, timestamp: new Date().getTime() }; if (this.content.length > 2000) { try { state.compressed = true; state.content = LZString.compressToUTF16(this.content); } catch (e) { console.error('压缩失败,使用原始数据', e); state.compressed = false; } } sessionStorage.setItem('medicalRecordState', JSON.stringify(state)); }, restoreState() { const savedState = sessionStorage.getItem('medicalRecordState'); if (!savedState) return; try { const state = JSON.parse(savedState); if (state.version !== this.stateVersion) { console.warn('状态版本不匹配,跳过恢复'); return; } let content = state.content; if (state.compressed) { try { content = LZString.decompressFromUTF16(content); } catch (e) { console.error('解压失败,使用原始数据', e); } } this.content = content; this.submittedId = state.submittedId; this.showRawHtml = state.showRawHtml; this.currentDnTags = state.currentDnTags || []; this.currentMnTags = state.currentMnTags || []; this.currentSnTags = state.currentSnTags || []; this.currentPnTags = state.currentPnTags || []; this.currentFnTags = state.currentFnTags || []; this.formData = state.formData || { mrname: "", mrorigin: "", mrdoctor: "", mrposter: "" }; console.log('医案状态已恢复'); } catch (e) { console.error('状态恢复失败', e); sessionStorage.removeItem('medicalRecordState'); } }, clearState() { sessionStorage.removeItem('medicalRecordState'); }, toggleDisplayFormat() { this.showRawHtml = !this.showRawHtml; this.saveState(); }, async fetchDNTags() { try { const response = await fetch('DNTag/?format=json'); const data = await response.json(); this.dnTags = data .map(item => item.dnname) .filter(name => name && name.trim().length > 0); this.dnTagMap = {}; data.forEach(item => { if (item.id && item.dnname) { this.dnTagMap[item.id] = item.dnname; } }); } catch (error) { console.error('获取标签失败:', error); alert('标签数据加载失败,高亮功能不可用'); } }, async fetchMNTags() { try { const response = await fetch('MNTag/?format=json'); const data = await response.json(); this.mnTags = data .map(item => item.mnname) .filter(name => name && name.trim().length > 0); this.mnTagMap = {}; data.forEach(item => { if (item.id && item.mnname) { this.mnTagMap[item.id] = item.mnname; } }); } catch (error) { console.error('获取草本标签失败:', error); alert('草本标签数据加载失败,高亮功能不可用'); } }, async fetchById() { if (!this.fetchId) { alert('請輸入有效的醫案ID'); return; } try { const response = await fetch(`MRInfo/${this.fetchId}/?format=json`); if (response.ok) { const data = await response.json(); this.formData = { mrname: data.mrname || '', mrorigin: data.mrorigin || '', mrdoctor: data.mrdoctor || '', mrposter: data.mrposter || '' }; this.content = data.mrcase || ''; this.submittedId = data.id; this.fetchId = null; if (data.dntag && Array.isArray(data.dntag)) { this.currentDnTags = data.dntag.map(id => this.dnTagMap[id] || `未知標籤(${id})` ); } else { this.currentDnTags = []; } if (data.mntag && Array.isArray(data.mntag)) { this.currentMnTags = data.mntag.map(id => this.mnTagMap[id] || `未知草本(${id})` ); } else { this.currentMnTags = []; } if (data.sntag && Array.isArray(data.sntag)) { this.currentSnTags = data.sntag.map(id => this.snTagMap[id] || `未知典籍(${id})` ); } else { this.currentSnTags = []; } if (data.pntag && Array.isArray(data.pntag)) { this.currentPnTags = data.pntag.map(id => this.pnTagMap[id] || `未知方劑(${id})` ); } else { this.currentPnTags = []; } if (data.fntag && Array.isArray(data.fntag)) { this.currentFnTags = data.fntag.map(id => this.fnTagMap[id] || `未知人物(${id})` ); } else { this.currentFnTags = []; } await this.fetchDNTags(); await this.fetchMNTags(); await this.fetchSNTags(); await this.fetchPNTags(); await this.fetchFNTags(); this.saveState(); alert('醫案數據加載成功!'); } else if (response.status === 404) { alert('未找到該ID的醫案'); } else { throw new Error('獲取醫案失敗'); } } catch (error) { console.error('Error:', error); alert(`獲取醫案失敗: ${error.message}`); } }, async submitContent() { const postData = { mrcase: this.content, mrname: this.formData.mrname, mrorigin: this.formData.mrorigin, mrdoctor: this.formData.mrdoctor, mrposter: this.formData.mrposter }; try { const response = await fetch('MRInfo/?format=json', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(postData), }); if (response.ok) { const data = await response.json(); this.submittedId = data.id; if (data.dntag && Array.isArray(data.dntag)) { this.currentDnTags = data.dntag.map(id => this.dnTagMap[id] || `未知標籤(${id})` ); } else { this.currentDnTags = []; } if (data.mntag && Array.isArray(data.mntag)) { this.currentMnTags = data.mntag.map(id => this.mnTagMap[id] || `未知草本(${id})` ); } else { this.currentMnTags = []; } if (data.sntag && Array.isArray(data.sntag)) { this.currentSnTags = data.sntag.map(id => this.snTagMap[id] || `未知典籍(${id})` ); } else { this.currentSnTags = []; } if (data.pntag && Array.isArray(data.pntag)) { this.currentPnTags = data.pntag.map(id => this.pnTagMap[id] || `未知方劑(${id})` ); } else { this.currentPnTags = []; } if (data.fntag && Array.isArray(data.fntag)) { this.currentFnTags = data.fntag.map(id => this.fnTagMap[id] || `未知人物(${id})` ); } else { this.currentFnTags = []; } this.saveState(); alert('醫案提交成功!'); } else { throw new Error('提交失败'); } } catch (error) { console.error('Error:', error); alert(`提交失败: ${error.message}`); } }, async updateContent() { if (!this.submittedId) return; const postData = { mrcase: this.content, mrname: this.formData.mrname, mrorigin: this.formData.mrorigin, mrdoctor: this.formData.mrdoctor, mrposter: this.formData.mrposter }; try { const response = await fetch(`MRInfo/${this.submittedId}/?format=json`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(postData), }); if (response.ok) { const data = await response.json(); if (data.dntag && Array.isArray(data.dntag)) { this.currentDnTags = data.dntag.map(id => this.dnTagMap[id] || `未知標籤(${id})` ); } if (data.mntag && Array.isArray(data.mntag)) { this.currentMnTags = data.mntag.map(id => this.mnTagMap[id] || `未知草本(${id})` ); } if (data.sntag && Array.isArray(data.sntag)) { this.currentSnTags = data.sntag.map(id => this.snTagMap[id] || `未知典籍(${id})` ); } if (data.pntag && Array.isArray(data.pntag)) { this.currentPnTags = data.pntag.map(id => this.pnTagMap[id] || `未知方劑(${id})` ); } if (data.fntag && Array.isArray(data.fntag)) { this.currentFnTags = data.fntag.map(id => this.fnTagMap[id] || `未知人物(${id})` ); } this.saveState(); alert('醫案更新成功!'); } else { throw new Error('更新失败'); } } catch (error) { console.error('Error:', error); alert(`更新失败: ${error.message}`); } }, async deleteContent() { if (!this.submittedId) return; if (!confirm('確定要刪除這個醫案嗎?')) return; try { const response = await fetch(`MRInfo/${this.submittedId}/?format=json`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', } }); if (response.ok) { this.clearState(); this.resetAll(); alert('醫案刪除成功!'); } else { throw new Error('刪除失败'); } } catch (error) { console.error('Error:', error); alert(`刪除失败: ${error.message}`); } } } }; </script> <style scoped> .wangeditor { flex: 1; padding: 10px; overflow-y: auto; } .up-panel { position: fixed; top: 55px; right: 0; width: 50%; height: 45%; padding: 0; overflow: hidden; background: #ffe6d4; display: block; } .tag-panel { width: 80%; height: 100%; background: lightyellow; padding: 10px; overflow-y: auto; display: none; z-index: 100; } .crud-buttons { position: absolute; top: 0; right: 0; display: flex; flex-direction: column; z-index: 101; gap: 5px; } .fixed-dncrud-btn { background-color: #EF6C00; color: white; border: none; padding: 8px 15px; border-radius: 4px; cursor: pointer; width: 120px; text-align: center; } .fixed-dncrud-btn:hover { background-color: #E65100; } .fixed-mncrud-btn { background-color: #2E7D32; color: white; border: none; padding: 8px 15px; border-radius: 4px; cursor: pointer; width: 120px; text-align: center; } .fixed-mncrud-btn:hover { background-color: #1B5E20; } .fixed-sncrud-btn { background-color: #1976D2; color: white; border: none; padding: 8px 15px; border-radius: 4px; cursor: pointer; width: 120px; text-align: center; } .fixed-sncrud-btn:hover { background-color: #0D47A1; } .fixed-pncrud-btn { background-color: #8A2BE2; color: white; border: none; padding: 8px 15px; border-radius: 4px; cursor: pointer; width: 120px; text-align: center; } .fixed-pncrud-btn:hover { background-color: #6A1B9A; } .fixed-fncrud-btn { background-color: #8B4513; color: white; border: none; padding: 8px 15px; border-radius: 4px; cursor: pointer; width: 120px; text-align: center; } .fixed-fncrud-btn:hover { background-color: #654321; } .right-panel { position: fixed; bottom: 45px; right: 0; width: 50%; height: 43%; background: lightblue; padding: 1px; z-index: 100; overflow-y: auto; } .dntag-display { display: flex; margin-top: 1px; padding: 10px; background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); } .mntag-display { display: flex; margin-top: 10px; padding: 10px; background: #f0f9eb; border: 1px solid #e1f3d8; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); } .sntag-display { display: flex; margin-top: 10px; padding: 10px; background: #e3f2fd; border: 1px solid #bbdefb; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); } .pntag-display { display: flex; margin-top: 10px; padding: 10px; background: #f3e5f5; border: 1px solid #e1bee7; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); } .fntag-display { display: flex; margin-top: 10px; padding: 10px; background: #f5f0e6; border: 1px solid #d2b48c; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); } .field-name { font-weight: bold; min-width: 80px; color: #495057; font-size: 1rem; } .dntag-value, .mntag-value, .sntag-value, .pntag-value, .fntag-value { flex-grow: 1; padding-left: 10px; } .dntag-value { border-left: 2px solid #ffc107; } .mntag-value { border-left: 2px solid #4CAF50; } .sntag-value { border-left: 2px solid #2196F3; } .pntag-value { border-left: 2px solid #8A2BE2; } .fntag-value { border-left: 2px solid #8B4513; } .dn-tag { color: rgb(212, 107, 8); font-weight: bold; margin-right: 2px; } .mn-tag { color: rgb(0, 128, 0); font-weight: bold; margin-right: 2px; } .sn-tag { color: rgb(51, 102, 255); font-weight: bold; margin-right: 2px; } .pn-tag { color: rgb(138, 43, 226); font-weight: bold; margin-right: 2px; } .fn-tag { color: rgb(139, 69, 19); font-weight: bold; margin-right: 2px; } .content-display { position: fixed; top: 350px; left: 0; width: 50%; height: 60%; bottom: 45px; z-index: 999; background-color: white; overflow-y: auto; padding: 10px; border: 1px solid #eee; white-space: pre-wrap; } .sticky-footer { display: flex; justify-content: flex-start; align-items: center; position: fixed; bottom: 0; left: 0; width: 100%; background-color: #ffd800ff; z-index: 999; padding: 10px 20px; box-sizing: border-box; flex-wrap: wrap; } .sticky-footer > span { margin-left: 5px; display: flex; align-items: center; } .submitted-id { padding: 2px; background-color: #e2f0fd; color: #004085; border-radius: 4px; } .reset-btn { margin-left: 10px; padding: 2px; background-color: #dc3545; border: none; color: white; border-radius: 4px; cursor: pointer; } .reset-btn:hover { background-color: #c82333; } .id-input { display: flex; align-items: center; } .id-input input { width: 100px; padding: 2px 5px; margin-right: 5px; border: 1px solid #ccc; border-radius: 4px; } .submit-btn { background-color: #28a745; color: white; border: none; padding: 2px 8px; border-radius: 4px; cursor: pointer; } .update-btn { background-color: #007bff; color: white; border: none; padding: 2px 8px; border-radius: 4px; cursor: pointer; } .delete-btn { background-color: #dc3545; color: white; border: none; padding: 2px 8px; border-radius: 4px; cursor: pointer; } .submit-btn:hover { background-color: #218838; } .update-btn:hover { background-color: #0069d9; } .delete-btn:hover { background-color: #c82333; } .form-input { margin-right: 10px; } .name-input, .origin-input, .doctor-input, .poster-input { padding: 5px 8px; border: 1px solid #ccc; border-radius: 4px; width: 150px; } </style> mrdncrud: <template> <div class="dncrud-container"> <div class="dncrud-controls"> <input v-model="searchQuery" placeholder="搜索病態名稱..." class="search-input" @input="filterDNTags" /> </div> <!-- 创建表单 --> <div class="form-section"> <form @submit.prevent="createDNTag" class="create-form"> <input v-model="newDNTag.dnname" placeholder="輸入新病態名稱" required class="form-input" :disabled="isLoading" /> <button type="submit" class="form-btn create-btn" :disabled="isLoading">創建</button> </form> </div> <div class="dncrud-content"> <div v-if="isLoading" class="loading"> <div class="loading-spinner"></div> <span>加載中...</span> </div> <div v-else> <!-- 标签列表 --> <div class="dncrud-list" v-if="filteredTags.length > 0"> <div v-for="tag in filteredTags" :key="tag.id" class="dncrud-item"> <div class="tag-info"> <span class="tag-id">ID: {{ tag.id }}</span> <span class="tag-name">{{ tag.dnname }}</span> </div> <div class="tag-actions"> <button @click="editDNTag(tag)" class="action-btn edit-btn">編輯</button> <button @click="deleteDNTag(tag.id)" class="action-btn delete-btn">刪除</button> </div> </div> </div> <div v-else class="no-tags"> 沒有找到匹配的病態標籤 </div> </div> </div> <!-- 编辑模态框 --> <div v-if="editingTag" class="edit-modal"> <div class="modal-content"> <h3>編輯病態名稱</h3> <form @submit.prevent="updateDNTag"> <input v-model="editingTag.dnname" required class="form-input" :disabled="isLoading" /> <div class="modal-actions"> <button type="submit" class="form-btn save-btn">保存</button> <button type="button" @click="cancelEdit" class="form-btn cancel-btn">取消</button> </div> </form> </div> </div> </div> </template> <script> export default { name: 'mrdncrud', data() { return { dnTags: [], filteredTags: [], searchQuery: '', newDNTag: { dnname: '' }, editingTag: null, isLoading: false }; }, methods: { async fetchDNTags() { this.isLoading = true; try { const response = await fetch('DNTag/?format=json'); if (!response.ok) throw new Error('获取数据失败'); this.dnTags = await response.json(); this.filterDNTags(); } catch (error) { console.error('获取病态标签失败:', error); alert('加载数据失败,请重试'); } finally { this.isLoading = false; } }, filterDNTags() { const query = this.searchQuery.trim().toLowerCase(); if (!query) { this.filteredTags = [...this.dnTags]; } else { this.filteredTags = this.dnTags.filter(tag => tag.dnname.toLowerCase().includes(query) || tag.id.toString().includes(query) ); } }, async createDNTag() { if (!this.newDNTag.dnname.trim()) { alert('病态名称不能为空'); return; } this.isLoading = true; try { const response = await fetch('DNTag/?format=json', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(this.newDNTag), }); if (!response.ok) throw new Error('创建失败'); await this.fetchDNTags(); this.newDNTag.dnname = ''; this.$emit('tag-updated'); // 通知父组件标签已更新 } catch (error) { console.error('创建病态标签失败:', error); alert(error.message); } finally { this.isLoading = false; } }, editDNTag(tag) { this.editingTag = { ...tag }; }, async updateDNTag() { if (!this.editingTag.dnname.trim()) { alert('病态名称不能为空'); return; } this.isLoading = true; try { const response = await fetch(`DNTag/${this.editingTag.id}/?format=json`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(this.editingTag), }); if (!response.ok) throw new Error('更新失败'); await this.fetchDNTags(); this.editingTag = null; this.$emit('tag-updated'); // 通知父组件标签已更新 } catch (error) { console.error('更新病态标签失败:', error); alert(error.message); } finally { this.isLoading = false; } }, cancelEdit() { this.editingTag = null; }, async deleteDNTag(id) { if (!confirm('确定要删除这个病态标签吗?')) return; this.isLoading = true; try { const response = await fetch(`DNTag/${id}/?format=json`, { method: 'DELETE' }); if (!response.ok) throw new Error('删除失败'); await this.fetchDNTags(); this.$emit('tag-updated'); // 通知父组件标签已更新 } catch (error) { console.error('删除病态标签失败:', error); alert(error.message); } finally { this.isLoading = false; } } }, mounted() { this.fetchDNTags(); } }; </script> <style scoped> .dncrud-container { height: 100%; display: flex; flex-direction: column; } .dncrud-controls { padding: 10px; background: #f5f5f5; border-bottom: 1px solid #ddd; } .search-input { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; } .dncrud-content { flex: 1; overflow-y: auto; padding: 10px; } .form-section { padding: 10px; background: #f5f5f5; border-bottom: 1px solid #ddd; } .create-form { display: flex; gap: 10px; } .form-input { flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px; } .form-btn { padding: 8px 15px; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; } .create-btn { background-color: #4CAF50; color: white; } .dncrud-list { display: flex; flex-direction: column; gap: 10px; } .dncrud-item { display: flex; justify-content: space-between; align-items: center; padding: 10px; background: white; border: 1px solid #eee; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } .tag-info { display: flex; flex-direction: column; } .tag-id { font-size: 0.8rem; color: #666; } .tag-name { font-weight: 500; } .tag-actions { display: flex; gap: 5px; } .action-btn { padding: 5px 10px; border: none; border-radius: 4px; cursor: pointer; font-size: 0.9rem; } .edit-btn { background-color: #2196F3; color: white; } .delete-btn { background-color: #f44336; color: white; } .no-tags { padding: 20px; text-align: center; color: #666; font-style: italic; } .edit-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); display: flex; justify-content: center; align-items: center; z-index: 1000; } .modal-content { background: white; padding: 20px; border-radius: 8px; width: 90%; max-width: 400px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); } .modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 15px; } .save-btn { background-color: #4CAF50; color: white; } .cancel-btn { background-color: #9E9E9E; color: white; } .loading { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20px; } .loading-spinner { border: 4px solid rgba(0, 0, 0, 0.1); border-radius: 50%; border-top: 4px solid #3498db; width: 30px; height: 30px; animation: spin 1s linear infinite; margin-bottom: 10px; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style>
08-03
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值